This commit is contained in:
chaos-zhu
2022-12-12 20:35:52 +08:00
parent 0d0b24de75
commit bcfbe0d6b3
28 changed files with 523 additions and 546 deletions
+11
View File
@@ -1,3 +1,14 @@
## [1.2.1](https://github.com/chaos-zhu/easynode/releases) (2022-12-12)
### Features
* 新增支持终端长命令输入模式 ✔
* 新增前端静态文件缓存 ✔
### Bug Fixes
* v1.2的若干bug...
## [1.2.0](https://github.com/chaos-zhu/easynode/releases) (2022-09-12)
### Features
+4 -6
View File
@@ -1,14 +1,12 @@
# 用于收集一些疑难杂症
# Q&A
- **欢迎pr~**
## 甲骨文CentOS7/8启动服务失败
## CentOS7/8启动服务失败
> 先关闭SELinux
```shell
vi /etc/selinux/config
SELINUX=enforcing
SELINUX=enforcing
// 修改为禁用
SELINUX=disabled
```
@@ -17,6 +15,6 @@ SELINUX=disabled
> 查看SELinux状态:sestatus
## 甲骨文ubuntu20.04客户端服务启动成功,无法连接?
## 客户端服务启动成功,无法连接?
> 端口未开放:`iptables -I INPUT -s 0.0.0.0/0 -p tcp --dport 22022 -j ACCEPT` 或者 `rm -rf /etc/iptables && reboot`
+1 -9
View File
@@ -42,12 +42,10 @@
- 依赖Node.js环境
- 占用端口:8082(http端口)、8083(https端口)、22022(客户端端口)
- 占用端口:8082(http端口)、22022(客户端端口)
- 建议使用**境外服务器**(最好延迟低)安装服务端,客户端信息监控与webssh功能都将以`该服务器作为跳板机`
- https服务需自行配置证书,或者使用`nginx反代`解决(推荐)
#### Docker镜像
> 注意:网速统计功能可能受限,docker网络将使用host模式(与宿主机共享端口,占用: 8082、22022)
@@ -88,12 +86,6 @@ wget -qO- --no-check-certificate https://ghproxy.com/https://raw.githubuserconte
- 默认登录密码:admin(首次部署完成后请及时修改).
6. 部署https服务
- 部署https服务需要自己上传域名证书至`\server\app\config\pem`,并且证书和私钥分别命名:`key.pem`和`cert.pem`
- 配置域名:vim server/app/config/index.js 在domain字段中填写你解析到服务器的域名
- pm2 restart easynode-server
- 不出意外你就可以访问https服务:https://domain:8083
---
### 客户端安装
Regular → Executable
View File
+8 -1
View File
@@ -73,9 +73,16 @@ mv ${FILE_PATH}/${SERVER_NAME}.service ${SERVICE_PATH}
# echo "***********************daemon-reload***********************"
systemctl daemon-reload
echo "***********************启动服务***********************"
echo "***********************准备启动服务***********************"
systemctl start ${SERVER_NAME}
if [ $? != 0 ]
then
echo "***********************${SERVER_NAME}.service启动失败***********************"
echo "***********************可能是服务器开启了SELinux, 参见Q&A***********************"
exit 1
fi
echo "***********************服务启动成功***********************"
# echo "***********************设置开机启动***********************"
systemctl enable ${SERVER_NAME}
+1 -15
View File
@@ -1,25 +1,11 @@
const path = require('path')
const fs = require('fs')
const getCertificate =() => {
try {
return {
cert: fs.readFileSync(path.join(__dirname, './pem/cert.pem')),
key: fs.readFileSync(path.join(__dirname, './pem/key.pem'))
}
} catch (error) {
return null
}
}
module.exports = {
domain: 'xxx.com', // https域名, 可不配置
httpPort: 8082,
httpsPort: 8083,
clientPort: 22022, // 勿更改
certificate: getCertificate(),
uploadDir: path.join(process.cwd(),'app/static/upload'),
staticDir: path.join(process.cwd(),'app/static'),
sftpCacheDir: path.join(process.cwd(),'app/socket/.sftp-cache'),
sftpCacheDir: path.join(process.cwd(),'app/socket/sftp-cache'),
sshRecordPath: path.join(process.cwd(),'app/storage/ssh-record.json'),
keyPath: path.join(process.cwd(),'app/storage/key.json'),
hostListPath: path.join(process.cwd(),'app/storage/host-list.json'),
View File
+1 -3
View File
@@ -1,6 +1,6 @@
const consola = require('consola')
global.consola = consola
const { httpServer, httpsServer, clientHttpServer } = require('./server')
const { httpServer, clientHttpServer } = require('./server')
const initLocal = require('./init')
const scheduleJob = require('./schedule')
@@ -10,6 +10,4 @@ initLocal()
httpServer()
httpsServer()
clientHttpServer()
+11 -11
View File
@@ -1,12 +1,12 @@
const koaBody = require('koa-body')
const { uploadDir } = require('../config')
module.exports = koaBody({
multipart: true, // 支持 multipart-formdate 的表单
formidable: {
uploadDir, // 上传目录
keepExtensions: true, // 保持文件的后缀
multipart: true, // 多文件上传
maxFieldsSize: 2 * 1024 * 1024 // 文件上传大小 单位:B
}
const koaBody = require('koa-body')
const { uploadDir } = require('../config')
module.exports = koaBody({
multipart: true, // 支持 multipart-formdate 的表单
formidable: {
uploadDir, // 上传目录
keepExtensions: true, // 保持文件的后缀
multipart: true, // 多文件上传
maxFieldsSize: 2 * 1024 * 1024 // 文件上传大小 单位:B
}
})
-2
View File
@@ -1,10 +1,8 @@
const cors = require('@koa/cors')
// const { domain } = require('../config')
// 跨域处理
const useCors = cors({
origin: ({ req }) => {
// return domain || req.headers.origin
return req.headers.origin
},
credentials: true,
+57 -57
View File
@@ -1,58 +1,58 @@
const log4js = require('log4js')
const { outDir, recordLog } = require('../config').logConfig
log4js.configure({
appenders: {
// 控制台输出
out: {
type: 'stdout',
layout: {
type: 'colored'
}
},
// 保存日志文件
cheese: {
type: 'file',
maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes
filename: `${ outDir }/receive.log`
}
},
categories: {
default: {
appenders: [ 'out', 'cheese' ], // 配置
level: 'info' // 只输出info以上级别的日志
}
}
// pm2: true
})
const logger = log4js.getLogger()
const useLog = () => {
return async (ctx, next) => {
const { method, path, origin, query, body, headers, ip } = ctx.request
const data = {
method,
path,
origin,
query,
body,
ip,
headers
}
await next() // 等待路由处理完成,再开始记录日志
// 是否记录日志
if (recordLog) {
const { status, params } = ctx
data.status = status
data.params = params
data.result = ctx.body || 'no content'
if (String(status).startsWith(4) || String(status).startsWith(5))
logger.error(JSON.stringify(data))
else
logger.info(JSON.stringify(data))
}
}
}
const log4js = require('log4js')
const { outDir, recordLog } = require('../config').logConfig
log4js.configure({
appenders: {
// 控制台输出
out: {
type: 'stdout',
layout: {
type: 'colored'
}
},
// 保存日志文件
cheese: {
type: 'file',
maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes
filename: `${ outDir }/receive.log`
}
},
categories: {
default: {
appenders: [ 'out', 'cheese' ], // 配置
level: 'info' // 只输出info以上级别的日志
}
}
// pm2: true
})
const logger = log4js.getLogger()
const useLog = () => {
return async (ctx, next) => {
const { method, path, origin, query, body, headers, ip } = ctx.request
const data = {
method,
path,
origin,
query,
body,
ip,
headers
}
await next() // 等待路由处理完成,再开始记录日志
// 是否记录日志
if (recordLog) {
const { status, params } = ctx
data.status = status
data.params = params
data.result = ctx.body || 'no content'
if (String(status).startsWith(4) || String(status).startsWith(5))
logger.error(JSON.stringify(data))
else
logger.info(JSON.stringify(data))
}
}
}
module.exports = useLog()
+13 -13
View File
@@ -1,13 +1,13 @@
const router = require('../router')
// 路由中间件
const useRoutes = router.routes()
// 优化错误提示中间件
// 原先如果请求方法错误响应404
// 使用该中间件后请求方法错误会提示405 Method Not Allowed【get list ✔200 post /list ❌405】
const useAllowedMethods = router.allowedMethods()
module.exports = {
useRoutes,
useAllowedMethods
}
const router = require('../router')
// 路由中间件
const useRoutes = router.routes()
// 优化错误提示中间件
// 原先如果请求方法错误响应404
// 使用该中间件后请求方法错误会提示405 Method Not Allowed【get list ✔200 post /list ❌405】
const useAllowedMethods = router.allowedMethods()
module.exports = {
useRoutes,
useAllowedMethods
}
+14 -14
View File
@@ -1,14 +1,14 @@
const koaStatic = require('koa-static')
const { staticDir } = require('../config')
const useStatic = koaStatic(staticDir, {
maxage: 1000 * 60 * 60 * 24 * 30,
gzip: true,
setHeaders: (res, path) => {
if(path && path.endsWith('.html')) {
res.setHeader('Cache-Control', 'max-age=0')
}
}
})
module.exports = useStatic
const koaStatic = require('koa-static')
const { staticDir } = require('../config')
const useStatic = koaStatic(staticDir, {
maxage: 1000 * 60 * 60 * 24 * 30,
gzip: true,
setHeaders: (res, path) => {
if(path && path.endsWith('.html')) {
res.setHeader('Cache-Control', 'max-age=0')
}
}
})
module.exports = useStatic
+55 -68
View File
@@ -1,68 +1,55 @@
const Koa = require('koa')
const compose = require('koa-compose') // 组合中间件,简化写法
const http = require('http')
const https = require('https')
const { clientPort } = require('./config')
const { domain, httpPort, httpsPort, certificate } = require('./config')
const middlewares = require('./middlewares')
const wsMonitorOsInfo = require('./socket/monitor')
const wsTerminal = require('./socket/terminal')
const wsSftp = require('./socket/sftp')
const wsHostStatus = require('./socket/host-status')
const wsClientInfo = require('./socket/clients')
const { throwError } = require('./utils')
const httpServer = () => {
const app = new Koa()
const server = http.createServer(app.callback())
serverHandler(app, server)
// ws一直报跨域的错误:参照官方文档使用createServer API创建服务
server.listen(process.env.PORT || httpPort, () => {
consola.success(`Server(http) is running on: http://localhost:${ httpPort }`)
})
}
const httpsServer = () => {
if(!certificate) return consola.error('未上传证书, 创建https服务失败')
const app = new Koa()
const server = https.createServer(certificate, app.callback())
serverHandler(app, server)
server.listen(httpsPort, (err) => {
if (err) return consola.error('https server error: ', err)
consola.success(`Server(https) is running: https://${ domain }:${ httpsPort }`)
})
}
const clientHttpServer = () => {
const app = new Koa()
const server = http.createServer(app.callback())
wsMonitorOsInfo(server) // 监控本机信息
server.listen(clientPort, () => {
consola.success(`Client(http) is running on: http://localhost:${ clientPort }`)
})
}
// 服务
function serverHandler(app, server) {
app.proxy = true // 用于nginx反代时获取真实客户端ip
wsTerminal(server) // 终端
wsSftp(server) // sftp
wsHostStatus(server) // 终端侧边栏host信息
wsClientInfo(server) // 客户端信息
app.context.throwError = throwError // 常用方法挂载全局ctx上
app.use(compose(middlewares))
// 捕获error.js模块抛出的服务错误
app.on('error', (err, ctx) => {
ctx.status = 500
ctx.body = {
status: ctx.status,
message: `Program Error:${ err.message }`
}
})
}
module.exports = {
httpServer,
httpsServer,
clientHttpServer
}
const Koa = require('koa')
const compose = require('koa-compose') // 组合中间件,简化写法
const http = require('http')
const { clientPort } = require('./config')
const { httpPort } = require('./config')
const middlewares = require('./middlewares')
const wsMonitorOsInfo = require('./socket/monitor')
const wsTerminal = require('./socket/terminal')
const wsSftp = require('./socket/sftp')
const wsHostStatus = require('./socket/host-status')
const wsClientInfo = require('./socket/clients')
const { throwError } = require('./utils')
const httpServer = () => {
const app = new Koa()
const server = http.createServer(app.callback())
serverHandler(app, server)
// ws一直报跨域的错误:参照官方文档使用createServer API创建服务
server.listen(httpPort, () => {
consola.success(`Server(http) is running on: http://localhost:${ httpPort }`)
})
}
const clientHttpServer = () => {
const app = new Koa()
const server = http.createServer(app.callback())
wsMonitorOsInfo(server) // 监控本机信息
server.listen(clientPort, () => {
consola.success(`Client(http) is running on: http://localhost:${ clientPort }`)
})
}
// 服务
function serverHandler(app, server) {
app.proxy = true // 用于nginx反代时获取真实客户端ip
wsTerminal(server) // 终端
wsSftp(server) // sftp
wsHostStatus(server) // 终端侧边栏host信息
wsClientInfo(server) // 客户端信息
app.context.throwError = throwError // 常用方法挂载全局ctx上
app.use(compose(middlewares))
// 捕获error.js模块抛出的服务错误
app.on('error', (err, ctx) => {
ctx.status = 500
ctx.body = {
status: ctx.status,
message: `Program Error:${ err.message }`
}
})
}
module.exports = {
httpServer,
clientHttpServer
}
+74 -74
View File
@@ -1,74 +1,74 @@
const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client')
const { clientPort } = require('../config')
const { verifyAuth } = require('../utils')
let hostSockets = {}
function getHostInfo(serverSocket, host) {
let hostSocket = ClientIO(`http://${ host }:${ clientPort }`, {
path: '/client/os-info',
forceNew: false,
timeout: 5000,
reconnectionDelay: 3000,
reconnectionAttempts: 100
})
// 将与客户端连接的socket实例保存起来,web端断开时关闭与客户端的连接
hostSockets[serverSocket.id] = hostSocket
hostSocket
.on('connect', () => {
consola.success('host-status-socket连接成功:', host)
hostSocket.on('client_data', (data) => {
serverSocket.emit('host_data', data)
})
hostSocket.on('client_error', () => {
serverSocket.emit('host_data', null)
})
})
.on('connect_error', (error) => {
consola.error('host-status-socket连接[失败]:', host, error.message)
serverSocket.emit('host_data', null)
})
.on('disconnect', () => {
consola.info('host-status-socket连接[断开]:', host)
serverSocket.emit('host_data', null)
})
}
module.exports = (httpServer) => {
const serverIo = new ServerIO(httpServer, {
path: '/host-status',
cors: {
origin: '*' // 需配置跨域
}
})
serverIo.on('connection', (serverSocket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address
serverSocket.on('init_host_data', ({ token, host }) => {
// 校验登录态
const { code, msg } = verifyAuth(token, clientIp)
if(code !== 1) {
serverSocket.emit('token_verify_fail', msg || '鉴权失败')
serverSocket.disconnect()
return
}
// 获取客户端数据
getHostInfo(serverSocket, host)
consola.info('host-status-socket连接socketId: ', serverSocket.id, 'host-status-socket已连接数: ', Object.keys(hostSockets).length)
// 关闭连接
serverSocket.on('disconnect', () => {
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
let socket = hostSockets[serverSocket.id]
socket.close && socket.close()
delete hostSockets[serverSocket.id]
consola.info('host-status-socket剩余连接数: ', Object.keys(hostSockets).length)
})
})
})
}
const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client')
const { clientPort } = require('../config')
const { verifyAuth } = require('../utils')
let hostSockets = {}
function getHostInfo(serverSocket, host) {
let hostSocket = ClientIO(`http://${ host }:${ clientPort }`, {
path: '/client/os-info',
forceNew: false,
timeout: 5000,
reconnectionDelay: 3000,
reconnectionAttempts: 100
})
// 将与客户端连接的socket实例保存起来,web端断开时关闭与客户端的连接
hostSockets[serverSocket.id] = hostSocket
hostSocket
.on('connect', () => {
consola.success('host-status-socket连接成功:', host)
hostSocket.on('client_data', (data) => {
serverSocket.emit('host_data', data)
})
hostSocket.on('client_error', () => {
serverSocket.emit('host_data', null)
})
})
.on('connect_error', (error) => {
consola.error('host-status-socket连接[失败]:', host, error.message)
serverSocket.emit('host_data', null)
})
.on('disconnect', () => {
consola.info('host-status-socket连接[断开]:', host)
serverSocket.emit('host_data', null)
})
}
module.exports = (httpServer) => {
const serverIo = new ServerIO(httpServer, {
path: '/host-status',
cors: {
origin: '*' // 需配置跨域
}
})
serverIo.on('connection', (serverSocket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address
serverSocket.on('init_host_data', ({ token, host }) => {
// 校验登录态
const { code, msg } = verifyAuth(token, clientIp)
if(code !== 1) {
serverSocket.emit('token_verify_fail', msg || '鉴权失败')
serverSocket.disconnect()
return
}
// 获取客户端数据
getHostInfo(serverSocket, host)
consola.info('host-status-socket连接socketId: ', serverSocket.id, 'host-status-socket已连接数: ', Object.keys(hostSockets).length)
// 关闭连接
serverSocket.on('disconnect', () => {
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
let socket = hostSockets[serverSocket.id]
socket.close && socket.close()
delete hostSockets[serverSocket.id]
consola.info('host-status-socket剩余连接数: ', Object.keys(hostSockets).length)
})
})
})
}
+71 -71
View File
@@ -1,71 +1,71 @@
const { Server } = require('socket.io')
const schedule = require('node-schedule')
const axios = require('axios')
let getOsData = require('../utils/os-data')
const consola = require('consola')
let serverSockets = {}, ipInfo = {}, osData = {}
async function getIpInfo() {
try {
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
consola.success('getIpInfo Success: ', new Date())
ipInfo = data
} catch (error) {
consola.error('getIpInfo Error: ', new Date(), error)
}
}
function ipSchedule() {
let rule1 = new schedule.RecurrenceRule()
rule1.second = [0, 10, 20, 30, 40, 50]
schedule.scheduleJob(rule1, () => {
let { query, country, city } = ipInfo || {}
if(query && country && city) return
consola.success('Task: start getIpInfo', new Date())
getIpInfo()
})
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
let rule2 = new schedule.RecurrenceRule()
rule2.hour = 2
rule2.minute = 0
rule2.second = 0
schedule.scheduleJob(rule2, () => {
consola.info('Task: refresh ip info', new Date())
getIpInfo()
})
}
ipSchedule()
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/client/os-info',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
// 存储对应websocket连接的定时器
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
} catch (error) {
consola.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1000)
socket.on('disconnect', () => {
// 断开时清楚对应的websocket连接
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
// console.log('断开socketId: ', socket.id, '剩余链接数: ', Object.keys(serverSockets).length)
})
})
}
const { Server } = require('socket.io')
const schedule = require('node-schedule')
const axios = require('axios')
let getOsData = require('../utils/os-data')
const consola = require('consola')
let serverSockets = {}, ipInfo = {}, osData = {}
async function getIpInfo() {
try {
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
consola.success('getIpInfo Success: ', new Date())
ipInfo = data
} catch (error) {
consola.error('getIpInfo Error: ', new Date(), error)
}
}
function ipSchedule() {
let rule1 = new schedule.RecurrenceRule()
rule1.second = [0, 10, 20, 30, 40, 50]
schedule.scheduleJob(rule1, () => {
let { query, country, city } = ipInfo || {}
if(query && country && city) return
consola.success('Task: start getIpInfo', new Date())
getIpInfo()
})
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
let rule2 = new schedule.RecurrenceRule()
rule2.hour = 2
rule2.minute = 0
rule2.second = 0
schedule.scheduleJob(rule2, () => {
consola.info('Task: refresh ip info', new Date())
getIpInfo()
})
}
ipSchedule()
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/client/os-info',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
// 存储对应websocket连接的定时器
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
} catch (error) {
consola.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1000)
socket.on('disconnect', () => {
// 断开时清楚对应的websocket连接
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
// console.log('断开socketId: ', socket.id, '剩余链接数: ', Object.keys(serverSockets).length)
})
})
}
+16 -16
View File
@@ -109,21 +109,21 @@ function listenInput(sftpClient, socket) {
socket.emit('sftp_error', error.message)
}
})
// socket.on('up_file', async ({ targetPath, fullPath, name, file }) => {
// console.log({ targetPath, fullPath, name, file })
// const exists = await sftpClient.exists(targetPath)
// if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
// try {
// const localPath = rawPath.join(sftpCacheDir, name)
// fs.writeFileSync(localPath, file)
// let res = await sftpClient.fastPut(localPath, fullPath)
// consola.success('sftp上传成功: ', res)
// socket.emit('up_file_success', res)
// } catch (error) {
// consola.error('up_file Error', error.message)
// socket.emit('sftp_error', error.message)
// }
// })
socket.on('up_file', async ({ targetPath, fullPath, name, file }) => {
console.log({ targetPath, fullPath, name, file })
const exists = await sftpClient.exists(targetPath)
if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
try {
const localPath = rawPath.join(sftpCacheDir, name)
fs.writeFileSync(localPath, file)
let res = await sftpClient.fastPut(localPath, fullPath)
consola.success('sftp上传成功: ', res)
socket.emit('up_file_success', res)
} catch (error) {
consola.error('up_file Error', error.message)
socket.emit('sftp_error', error.message)
}
})
/** 分片上传 */
// 1. 创建本地缓存文件夹
@@ -186,7 +186,7 @@ function listenInput(sftpClient, socket) {
clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件
} catch (error) {
consola.error('sftp上传失败: ', error.message)
socket.emit('sftp_error', error.message)
socket.emit('up_file_fail', error.message)
clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件
}
})
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+17 -17
View File
@@ -1,17 +1,17 @@
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
<title>EasyNode</title>
<script src="//at.alicdn.com/t/c/font_3309550_flid0kihu36.js"></script>
<script type="module" crossorigin src="/assets/index.eb5f280e.js"></script>
<link rel="stylesheet" href="/assets/index.a9194a35.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
<title>EasyNode</title>
<script src="//at.alicdn.com/t/c/font_3309550_flid0kihu36.js"></script>
<script type="module" crossorigin src="/assets/index.5de3ed69.js"></script>
<link rel="stylesheet" href="/assets/index.cf7d36d4.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+1 -2
View File
@@ -1,2 +1 @@
[
]
[]
+1 -1
View File
@@ -63,7 +63,7 @@ const getNetIPInfo = async (searchIp = '') => {
let { origip: ip, location: country, city = '', regionName = '' } = res || {}
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
}
console.log(searchResult)
// console.log(searchResult)
let validInfo = searchResult.find(item => Boolean(item.country))
consola.info('查询IP信息:', validInfo)
return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date }
Regular → Executable
View File