💥 兼容哪吒V1的数据接口

This commit is contained in:
hi2hi 2024-12-06 05:38:12 +00:00
parent cc585221c1
commit 1c6107cb07
17 changed files with 379 additions and 74 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
demo
# Editor directories and files
.vscode/*

View File

@ -1,3 +1,8 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ${DOMAIN};
@ -7,7 +12,18 @@ server {
proxy_pass ${NEZHA}ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 兼容哪吒V1
location /api/v1/ws/server {
proxy_pass ${NEZHA}api/v1/ws/server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -16,9 +16,13 @@ window.$$nazhuaConfig = {
// hideFilter: false, // 隐藏筛选
// hideTag: false, // 隐藏标签
// customCodeMap: {}, // 自定义的地图点信息
// nezhaVersion: 'v1', // 哪吒版本
// apiMonitorPath: '/api/v1/monitor/{id}',
// wsPath: '/ws',
// nezhaPath: '/nezha/',
// nezhaV0ConfigType: 'servers', // 哪吒v0数据读取类型
// v1ApiMonitorPath: '/api/v1/service/{id}',
// v1WsPath: '/api/v1/ws/server',
// v1GroupPath: '/api/v1/server-group',
// routeMode: 'h5', // 路由模式
};

View File

@ -55,7 +55,7 @@ async function wsReconnect() {
onMounted(async () => {
handleSystem();
refreshTime();
await store.dispatch('loadServers');
await store.dispatch('initServerInfo');
msg.on('close', () => {
console.log('ws closed');
wsReconnect();

View File

@ -12,10 +12,14 @@ const config = {
},
nazhua: {
title: '哪吒监控',
nezhaVersion: 'v0',
apiMonitorPath: '/api/v1/monitor/{id}',
wsPath: '/ws',
nezhaPath: '/nezha/',
nezhaV0ConfigType: 'servers',
v1ApiMonitorPath: '/api/v1/service/{id}',
v1WsPath: '/api/v1/ws/server',
v1GroupPath: '/api/v1/server-group',
// 解构载入自定义配置
...(window.$$nazhuaConfig || {}),
},

View File

@ -2,7 +2,13 @@ import {
createStore,
} from 'vuex';
import dayjs from 'dayjs';
import loadNezhaConfig from '@/utils/load-nezha-config';
import config from '@/config';
import loadNezhaV0Config, {
loadServerGroup as loadNezhaV0ServerGroup,
} from '@/utils/load-nezha-v0-config';
import {
loadServerGroup as loadNezhaV1ServerGroup,
} from '@/utils/load-nezha-v1-config';
import {
msg,
@ -10,6 +16,7 @@ import {
const defaultState = () => ({
init: false,
serverGroup: [],
serverList: [],
serverCount: {
total: 0,
@ -35,9 +42,13 @@ function handleServerCount(servers) {
return counts;
}
let firstSetServers = true;
const store = createStore({
state: defaultState(),
mutations: {
SET_SERVER_GROUP(state, serverGroup) {
state.serverGroup = serverGroup;
},
SET_SERVERS(state, servers) {
const newServers = [...servers];
newServers.sort((a, b) => b.DisplayIndex - a.DisplayIndex);
@ -58,8 +69,6 @@ const store = createStore({
};
if (oldItem?.PublicNote) {
serverItem.PublicNote = oldItem.PublicNote;
} else {
serverItem.PublicNote = {};
}
return serverItem;
});
@ -74,8 +83,20 @@ const store = createStore({
/**
* 加载服务器列表
*/
async loadServers({ commit }) {
const serverResult = await loadNezhaConfig();
async initServerInfo({ commit }) {
firstSetServers = true;
// 如果是v1版本的话加载v1版本的数据
if (config.nazhua.nezhaVersion === 'v1') {
loadNezhaV1ServerGroup().then((res) => {
if (res) {
commit('SET_SERVER_GROUP', res);
}
});
return;
}
// 如果是v0版本的话加载v0版本的数据
// 加载初始化的服务器列表,需要其中的公开备注字段
const serverResult = await loadNezhaV0Config();
if (!serverResult) {
console.error('load server config failed');
return;
@ -87,6 +108,11 @@ const store = createStore({
};
return item;
}) || [];
const res = loadNezhaV0ServerGroup(servers);
if (res) {
commit('SET_SERVER_GROUP', res);
}
firstSetServers = false;
commit('SET_SERVERS', servers);
},
/**
@ -104,8 +130,13 @@ const store = createStore({
};
return item;
}) || [];
if (firstSetServers) {
firstSetServers = false;
commit('SET_SERVERS', servers);
} else {
commit('UPDATE_SERVERS', servers);
}
}
});
},
},

View File

@ -38,3 +38,30 @@ export default async () => fetch(config.nazhua.nezhaPath).then((res) => res.text
}
return null;
}).catch(() => null);
/**
* 获取标签列表
*/
export const loadServerGroup = (services) => {
const tagMap = {};
services.forEach((i) => {
if (i.Tag) {
if (!tagMap[i.Tag]) {
tagMap[i.Tag] = [];
}
tagMap[i.Tag].push(i);
}
});
const tagList = [];
Object.entries(tagMap).forEach(([tag, serviceIds]) => {
tagList.push({
name: tag,
count: serviceIds.length,
servers: serviceIds,
group: {
name: tag,
},
});
});
return tagList;
};

View File

@ -0,0 +1,138 @@
/**
* V1版数据加载
*/
import store from '@/store';
import config from '@/config';
import request from '@/utils/request';
import { Mapping } from '@/utils/object-mapping';
/**
* 字段映射
*/
export const SERVER_FIELD_MAPS = {
ID: 'id',
CreatedAt: undefined,
UpdatedAt: undefined,
DeletedAt: undefined,
Name: 'name',
Tag: '_$function|queryGroup|id',
DisplayIndex: 'display_index',
HideForGuest: undefined,
EnableDDNS: undefined,
Host: '_$mapping|HOST_FIELD_MAPS',
State: '_$mapping|STATE_FIELD_MAPS',
LastActive: 'last_active',
};
export const HOST_FIELD_MAPS = {
Platform: 'host.platform',
PlatformVersion: 'host.platform_version',
CPU: 'host.cpu',
MemTotal: 'host.mem_total',
DiskTotal: 'host.disk_total',
SwapTotal: 'host.swap_total',
Arch: 'host.arch',
Virtualization: 'host.virtualization',
BootTime: 'host.boot_time',
CountryCode: 'country_code',
Version: 'host.version',
GPU: 'host.gpu',
};
export const STATE_FIELD_MAPS = {
CPU: 'state.cpu',
MemUsed: 'state.mem_used',
SwapUsed: 'state.swap_used',
DiskUsed: 'state.disk_used',
NetInTransfer: 'state.net_in_transfer',
NetOutTransfer: 'state.net_out_transfer',
NetInSpeed: 'state.net_in_speed',
NetOutSpeed: 'state.net_out_speed',
Uptime: 'state.uptime',
Load1: 'state.load_1',
Load5: 'state.load_5',
Load15: 'state.load_15',
TcpConnCount: 'state.tcp_conn_count',
UdpConnCount: 'state.udp_conn_count',
ProcessCount: 'state.process_count',
Temperatures: 'state.temperatures',
GPU: 'state.gpu',
};
/**
* 魔法方法
*/
const magics = {
HOST_FIELD_MAPS,
STATE_FIELD_MAPS,
queryGroup: (id) => {
const groupItem = store.state.serverGroup?.find?.((i) => {
if (i.servers) {
return i.servers.includes(id);
}
return false;
});
return groupItem?.name;
},
};
/**
* 处理V1版数据
* @param {Object} v1Data V1版数据
* @return {Object} V0版数据
*/
export const handelV1toV0 = (v1Data) => {
const v0Data = {};
Object.keys(SERVER_FIELD_MAPS).forEach((key) => {
if (SERVER_FIELD_MAPS[key] === undefined) {
return;
}
if (SERVER_FIELD_MAPS[key].includes('_$')) {
const $magic = SERVER_FIELD_MAPS[key].split('|');
switch ($magic[0]) {
case '_$function':
if ($magic.length >= 3 && magics[$magic[1]]) {
v0Data[key] = magics[$magic[1]](
Mapping.mapping(v1Data, $magic[2]),
);
} else {
v0Data[key] = undefined;
}
break;
case '_$mapping':
v0Data[key] = Mapping.each(magics[$magic[1]], v1Data);
break;
default:
break;
}
return;
}
v0Data[key] = Mapping.mapping(v1Data, SERVER_FIELD_MAPS[key]);
});
if (v1Data.public_note) {
try {
v0Data.PublicNote = JSON.parse(v1Data.public_note);
} catch (e) {
v1Data.PublicNote = null;
}
} else {
v1Data.PublicNote = null;
}
return v0Data;
};
export const loadServerGroup = async () => request({
url: config.nazhua.v1GroupPath,
type: 'GET',
}).then((res) => {
if (res.status === 200) {
const list = res.data?.data || [];
return list.map((i) => {
const item = {
...i,
name: i?.group?.name,
count: i?.servers?.length,
};
return item;
});
}
return null;
}).catch(() => null);

View File

@ -0,0 +1,99 @@
/**
* 对象映射封装
*/
class Mapping {
/**
* 字符串映射对象
*
* @param {Record<string, any>} obj 查找的对象
* @param {string} key 查找的属性
*
* @return {any}
*/
static mapping(obj, key) {
// 检查 obj 是否为对象,如果不是,返回 undefined
if (!obj || typeof obj !== 'object') {
return undefined;
}
// 检查 key 是否为字符串,如果不是,返回 undefined
if (typeof key !== 'string') {
return undefined;
}
// 检查 key 是否包含非法字符,如果包含,返回 undefined
if (key.includes('..') || key.startsWith('.') || key.endsWith('.')) {
return undefined;
}
// 如果 key 包含 '.',使用 reduce 方法递归获取嵌套属性值
if (key.includes('.')) {
return key.split('.').reduce((val, k) => (val !== undefined ? Mapping.get(val, k) : undefined), obj);
}
// 如果 key 不包含 '.',直接获取属性值
return Mapping.get(obj, key);
}
/**
* 获取数据
* 支持处理数组指针
* @param {Record<string, any> | any[]} obj 属性对象
* @param {string} key 属性名称
* @return {any}
*/
static get(obj, key) {
if (!obj || typeof obj !== 'object' || !key) {
return undefined;
}
const indexMatch = key.match(/\[(\d+)\]/);
if (indexMatch) {
const [fullMatch, indexStr] = indexMatch;
const index = Number(indexStr);
const matchIndex = key.indexOf(fullMatch);
if (matchIndex === 0) {
if (Array.isArray(obj) && index < obj.length) {
const val = obj[index];
const restKey = key.slice(fullMatch.length);
return restKey ? Mapping.get(val, restKey) : val;
}
} else {
const pre = key.slice(0, matchIndex);
const list = obj[pre];
if (Array.isArray(list) && index < list.length) {
const val = list[index];
const restKey = key.slice(matchIndex + fullMatch.length);
return restKey ? Mapping.get(val, restKey) : val;
}
}
return undefined;
}
return obj[key];
}
/**
* 数据根据key的映射进行组装
*
* @param {KeyMap} keys 映射对象
* @param {Record<string, unknown>} data 数据对象
*
* @return {Record<string, unknown>}
*/
static each(keys, data) {
// 检查 keys 是否为对象,如果不是,返回 undefined
if (!keys || typeof keys !== 'object') {
return undefined;
}
// 检查 data 是否为对象,如果不是,返回 undefined
if (!data || typeof data !== 'object') {
return undefined;
}
return Object.entries(keys).reduce((acc, [key, value]) => {
if (typeof value === 'string') {
acc[key] = Mapping.mapping(data, value);
}
return acc;
}, {});
}
}
const { mapping } = Mapping;
export { Mapping, mapping };
export default mapping;

View File

@ -1,40 +1,22 @@
import axios from 'axios';
import uuid from '@/utils/uuid';
import validate from '@/utils/validate';
import config from '@/config';
import CustomError from './custom-error';
const {
codeField,
dataField,
msgField,
okCode,
limit = 10,
} = config.request;
const limit = 10;
const requestTagMap = {};
/**
* axios请求
* @param {object} options 请求参数
* @param {boolean} noFormat 不进行返回数据的格式化处理 网络状态200即为成功
* @return {Promise}
*/
async function axiosRequest(options, noFormat) {
async function axiosRequest(options) {
return axios(options).then((res) => {
if (res.status === 200) {
if (noFormat) {
return res;
}
if (validate.isSet(res.data[codeField]) && `${res.data[codeField]}` === `${okCode}`) {
return res.data[dataField];
}
if (typeof res.data[codeField] !== 'undefined') {
throw new CustomError(res.data[msgField], res.data[codeField]);
}
throw new CustomError('服务器返回内容不规范', -99);
}
throw new CustomError(`网络错误${res.status}`, res.status);
});
}
@ -69,7 +51,6 @@ class NetworkRequest {
* @param {string} type 请求的Method
* @param {object} headers Header请求参数
* @param {object} data 请求参数
* @param {boolean} noFormat 不进行返回数据的格式化处理 网络状态200即为成功
* @param {boolean} defaultContentType 默认的请求方式
* @param {Boolean} priority 优先调用请求
*
@ -85,7 +66,6 @@ class NetworkRequest {
type,
headers,
data,
noFormat = false,
defaultContentType = true,
requestTag = undefined,
responseType,
@ -100,9 +80,7 @@ class NetworkRequest {
}
return new Promise((resolve, reject) => {
const defaultHeaders = {
...config.request.headers,
};
const defaultHeaders = {};
if (defaultContentType === false) {
if (NetworkRequest.FormRequest(defaultHeaders)) {
defaultHeaders['content-type'] = 'application/json';
@ -122,7 +100,6 @@ class NetworkRequest {
signal: abortController?.signal ?? undefined,
responseType,
},
noFormat,
(res) => {
resolve(res);
},
@ -153,13 +130,13 @@ class NetworkRequest {
if (this.tasks.length === 0) {
return;
}
const [options, onFormat, success, fail, tag] = this.tasks.pop();
const [options, success, fail, tag] = this.tasks.pop();
// 请求未执行已被中止
if (options?.signal?.aborted) {
this.overTask();
return;
}
requestTagMap[tag] = axiosRequest(options, onFormat);
requestTagMap[tag] = axiosRequest(options);
requestTagMap[tag].finally(() => {
this.overTask();
// 一秒内请求不重复

View File

@ -131,10 +131,13 @@ function switchPeakShaving() {
async function loadMonitor() {
await request({
url: config.nazhua.apiMonitorPath.replace('{id}', props.info.ID),
url: (
config.nazhua.nezhaVersion === 'v1' ? config.nazhua.v1ApiMonitorPath : config.nazhua.apiMonitorPath
).replace('{id}', props.info.ID),
}).then((res) => {
if (Array.isArray(res)) {
monitorData.value = res;
const list = config.nazhua.nezhaVersion === 'v1' ? res.data?.data : res.data?.result;
if (Array.isArray(list)) {
monitorData.value = list;
}
}).catch((err) => {
console.error(err);

View File

@ -45,8 +45,10 @@
>
<span class="text">
<span class="text-item value-text">{{ billAndPlan.billing.value }}</span>
<template v-if="!billAndPlan.billing.isFree">
<span class="text-item">/</span>
<span class="text-item label-text">{{ billAndPlan.billing.cycleLabel }}</span>
</template>
</span>
</div>
<div

View File

@ -64,6 +64,7 @@ export default (params) => {
}
}
if (validate.isSet(billingDataMod?.amount)) {
let isFree = false;
let amountValue = billingDataMod.amount;
let label;
if (billingDataMod.amount.toString() === '-1') {
@ -71,6 +72,7 @@ export default (params) => {
label = `${cycleLabel}`;
} else if (billingDataMod.amount.toString() === '0') {
amountValue = config.nazhua.freeAmount || '免费';
isFree = true;
} else {
label = `${cycleLabel}`;
}
@ -79,6 +81,7 @@ export default (params) => {
value: amountValue,
cycleLabel,
months,
isFree,
};
}
// 剩余时间

View File

@ -63,12 +63,12 @@ export default (params) => {
case 'cpu':
return {
type: 'cpu',
used: (props.info.State.CPU).toFixed(1) * 1,
used: (props.info.State?.CPU || 0).toFixed(1) * 1,
colors: {
used: '#0088ff',
total: 'rgba(255, 255, 255, 0.2)',
},
valText: `${(props.info.State.CPU).toFixed(1) * 1}%`,
valText: `${(props.info.State?.CPU || 0).toFixed(1) * 1}%`,
label: 'CPU',
content: {
default: cpuInfo.value?.core,

View File

@ -87,35 +87,10 @@ const serverList = computed(() => store.state.serverList);
//
const serverCount = computed(() => store.state.serverCount);
/**
* 解构数据
*/
const serverListData = computed(() => {
const tagMap = {};
serverList.value.forEach((i) => {
if (i.Tag) {
if (!tagMap[i.Tag]) {
tagMap[i.Tag] = 0;
}
tagMap[i.Tag] += 1;
}
});
const tags = [];
Object.entries(tagMap).forEach(([tag, count]) => {
tags.push({
tag,
count,
});
});
return {
tags,
};
});
const tagOptions = computed(() => (serverListData.value?.tags || []).map((i) => ({
const tagOptions = computed(() => store.state.serverGroup.map((i) => ({
key: uuid(),
label: i.tag,
value: i.tag,
label: i.name,
value: i.name,
})));
const onlineOptions = computed(() => {

View File

@ -1,10 +1,20 @@
import config from '@/config';
import MessageSubscribe from '@/utils/subscribe';
import {
handelV1toV0,
} from '@/utils/load-nezha-v1-config';
import WSService from './service';
function getWsApiPath() {
if (config.nazhua.nezhaVersion === 'v1') {
return config.nazhua.v1WsPath;
}
return config.nazhua.wsPath;
}
const msg = new MessageSubscribe();
const wsService = new WSService({
wsUrl: config?.nazhua?.wsPath,
wsUrl: getWsApiPath(),
onConnect: () => {
msg.emit('connect');
},
@ -16,7 +26,17 @@ const wsService = new WSService({
},
onMessage: (data) => {
if (data?.now) {
if (config.nazhua.nezhaVersion === 'v1') {
msg.emit('servers', {
now: data.now,
servers: data?.servers?.map?.((server) => {
const item = handelV1toV0(server);
return item;
}) || [],
});
} else {
msg.emit('servers', data);
}
} else {
msg.emit('message', data);
}

View File

@ -29,6 +29,11 @@ export default defineConfig({
changeOrigin: true,
ws: true,
},
'/api/v1/ws/server': {
target: process.env.WS_HOST,
changeOrigin: true,
ws: true,
},
'/nezha/': {
target: process.env.NEZHA_HOST,
changeOrigin: true,