This commit is contained in:
chaos-zhu 2022-12-12 20:32:08 +08:00
parent 0d0b24de75
commit bcfbe0d6b3
28 changed files with 523 additions and 546 deletions

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

10
Q&A.md
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`

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
---
### 客户端安装

0
client/bin/www Normal file → Executable file
View File

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}

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,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()

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

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,

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()

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
}

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

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
}

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

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

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.

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>

View File

@ -1,2 +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 }

0
server/bin/www Normal file → Executable file
View File