feat: 加入健康检查以及 pnpm start

This commit is contained in:
Li Wei 2025-06-11 10:22:33 +08:00
parent 2a84528345
commit 8f2711f728
19 changed files with 19156 additions and 13 deletions

Binary file not shown.

18231
backend/logs/app.log Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,14 @@
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"start": "NODE_ENV=production node dist/index.js",
"start:daemon": "NODE_ENV=production nohup node dist/index.js > logs/app.log 2>&1 &",
"start:bg": "NODE_ENV=production node dist/index.js &",
"stop": "pkill -f 'node.*dist/index.js'",
"dev": "ts-node src/index.ts",
"build": "tsc",
"watch": "tsc -w",
"logs": "tail -f logs/app.log",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],

View File

@ -8,6 +8,7 @@ import { config } from './config';
import monitoringRoutes from './controllers/monitoring';
import notificationRoutes from './controllers/notification';
import settingsRoutes from './routes/settings';
import systemHealthRoutes from './routes/systemHealth';
// 创建Koa应用
const app = new Koa();
@ -50,6 +51,7 @@ app.use(bodyParser());
router.use('/api/monitoring', monitoringRoutes.routes());
router.use('/api/notification', notificationRoutes.routes());
router.use('/api/settings', settingsRoutes.routes());
router.use('/api/system', systemHealthRoutes.routes());
// 使用路由中间件
app.use(router.routes());
@ -77,15 +79,44 @@ async function startServer() {
try {
// 初始化数据库连接
await AppDataSource.initialize();
console.log('数据库连接成功');
// 导入健康检查和监控服务
const { systemHealthService } = await import('./services/systemHealthService');
const { monitoringService } = await import('./services/monitoringService');
// 执行系统健康检查
console.log('执行系统健康检查...');
const health = await systemHealthService.checkSystemHealth();
// 显示健康检查结果
console.log(`系统健康状态: ${health.overall}`);
// 检查关键依赖
const criticalIssues = health.dependencies.filter(d => d.required && !d.available);
if (criticalIssues.length > 0) {
console.warn('⚠️ 发现关键依赖缺失:');
criticalIssues.forEach(issue => {
console.warn(` - ${issue.name}: ${issue.error}`);
const instructions = systemHealthService.getInstallInstructions(issue.name);
if (instructions !== '{}') {
console.warn(` 安装方法: ${instructions}`);
}
});
}
// 启动监控服务
await monitoringService.startAllMonitoring();
console.log('监控服务已启动');
// 启动服务器
app.listen(config.server.port, () => {
console.log(`服务器运行在 http://localhost:${config.server.port}`);
console.log(`系统整体健康状态: ${health.overall}`);
});
} catch (error) {
console.error('启动服务器失败:', error);
process.exit(1);
}
}

View File

@ -0,0 +1,29 @@
import Router from 'koa-router';
import { systemHealthService } from '../services/systemHealthService';
const router = new Router();
// 获取系统健康状态
router.get('/health', async (ctx) => {
try {
const health = await systemHealthService.checkSystemHealth();
ctx.body = { success: true, data: health };
} catch (error: any) {
ctx.status = 500;
ctx.body = { success: false, message: error.message };
}
});
// 获取依赖安装说明
router.get('/install/:dependency', async (ctx) => {
try {
const dependency = ctx.params.dependency;
const instructions = systemHealthService.getInstallInstructions(dependency);
ctx.body = { success: true, data: JSON.parse(instructions) };
} catch (error: any) {
ctx.status = 500;
ctx.body = { success: false, message: error.message };
}
});
export default router;

View File

@ -329,6 +329,12 @@ class MonitoringService {
});
} catch (err: any) {
console.error(`DNS 解析失败: ${err.message}`);
// 将DNS错误信息保存到结果中
result.dnsResolution = JSON.stringify({
error: `DNS解析失败: ${err.message}`,
errorCode: err.code || 'UNKNOWN',
timestamp: new Date().toISOString()
});
}
// 设置初始 traceroute 消息
@ -355,8 +361,8 @@ class MonitoringService {
await monitorRepository.save(monitor);
// 异步执行 traceroute,不阻塞主流程
this.executeTracerouteAsync(monitor, savedResult.id);
// 异步执行 traceroute(带降级策略),不阻塞主流程
this.executeTracerouteWithFallback(monitor, savedResult.id);
return savedResult;
}
@ -411,6 +417,123 @@ class MonitoringService {
}
}
// 检查traceroute工具可用性并使用降级策略
private async executeTracerouteWithFallback(monitor: Monitor, resultId: number) {
const resultRepository = AppDataSource.getRepository(MonitorResult);
// 尝试使用traceroute
const tracerouteAvailable = await this.checkCommandAvailable('traceroute');
if (tracerouteAvailable) {
return this.executeTracerouteAsync(monitor, resultId);
}
// Windows系统尝试tracert
if (process.platform === 'win32') {
const tracertAvailable = await this.checkCommandAvailable('tracert');
if (tracertAvailable) {
return this.executeTracertAsync(monitor, resultId);
}
}
// 降级到ping路由检测
return this.executePingRouteAsync(monitor, resultId);
}
private async checkCommandAvailable(command: string): Promise<boolean> {
try {
const { spawn } = require('child_process');
return new Promise((resolve) => {
const child = spawn(command, ['--help']);
child.on('error', () => resolve(false));
child.on('close', () => resolve(true));
setTimeout(() => {
child.kill();
resolve(false);
}, 5000);
});
} catch {
return false;
}
}
private async executeTracertAsync(monitor: Monitor, resultId: number) {
// Windows tracert实现
try {
const resultRepository = AppDataSource.getRepository(MonitorResult);
const { spawn } = require('child_process');
let output = '';
const child = spawn('tracert', ['-d', '-h', '5', monitor.host]);
this.tracerouteProcesses.set(monitor.id, { childProcess: child, resultId });
child.stdout.on('data', (data: Buffer) => {
output += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
output += data.toString();
});
child.on('close', async () => {
this.tracerouteProcesses.delete(monitor.id);
const result = await resultRepository.findOne({ where: { id: resultId } });
if (result) {
result.traceroute = output || 'Tracert 完成,但没有输出';
await resultRepository.save(result);
}
});
child.on('error', async (err: any) => {
this.tracerouteProcesses.delete(monitor.id);
const result = await resultRepository.findOne({ where: { id: resultId } });
if (result) {
result.traceroute = `Tracert 执行失败: ${err.message}`;
await resultRepository.save(result);
}
});
} catch (error: any) {
console.error('Tracert 执行失败:', error);
}
}
private async executePingRouteAsync(monitor: Monitor, resultId: number) {
// 使用ping作为降级方案
try {
const resultRepository = AppDataSource.getRepository(MonitorResult);
const { spawn } = require('child_process');
let output = '';
const pingArgs = process.platform === 'win32'
? ['-n', '5', monitor.host]
: ['-c', '5', monitor.host];
const child = spawn('ping', pingArgs);
child.stdout.on('data', (data: Buffer) => {
output += data.toString();
});
child.on('close', async () => {
const result = await resultRepository.findOne({ where: { id: resultId } });
if (result) {
result.traceroute = `网络连通性测试 (降级模式):\n${output}`;
await resultRepository.save(result);
}
});
child.on('error', async () => {
const result = await resultRepository.findOne({ where: { id: resultId } });
if (result) {
result.traceroute = '无法获取路由信息: 相关网络工具不可用';
await resultRepository.save(result);
}
});
} catch (error: any) {
console.error('降级路由检测失败:', error);
}
}
// 异步执行 traceroute
private async executeTracerouteAsync(monitor: Monitor, resultId: number) {
try {
@ -418,8 +541,8 @@ class MonitoringService {
// 根据操作系统选择命令
const cmd = process.platform === 'win32'
? `tracert -d -h 15 ${monitor.host}`
: `traceroute -n -m 15 ${monitor.host}`;
? `tracert -d -h 5 ${monitor.host}`
: `traceroute -n -m 5 ${monitor.host}`;
// 检查监控项是否仍处于激活状态
const monitorRepository = AppDataSource.getRepository(Monitor);
@ -439,10 +562,10 @@ class MonitoringService {
let command, args;
if (process.platform === 'win32') {
command = 'tracert';
args = ['-d', '-h', '15', monitor.host];
args = ['-d', '-h', '5', monitor.host];
} else {
command = 'traceroute';
args = ['-n', '-m', '15', monitor.host];
args = ['-n', '-m', '5', monitor.host];
}
// 创建子进程
@ -451,6 +574,25 @@ class MonitoringService {
// 将进程保存到 Map 中,以便能够在需要时终止它
this.tracerouteProcesses.set(monitor.id, { childProcess, resultId });
// 监听进程错误事件(如命令不存在等系统级错误)
childProcess.on('error', async (err: any) => {
console.error(`Traceroute 进程错误: ${err.message}`);
// 从 Map 中移除进程
this.tracerouteProcesses.delete(monitor.id);
// 更新数据库中的结果
const result = await resultRepository.findOne({ where: { id: resultId } });
if (result) {
let errorMessage = `Traceroute 执行失败: ${err.message}`;
if (err.code === 'ENOENT') {
errorMessage = 'Traceroute 命令不存在,请确保系统已安装 traceroute 工具';
}
result.traceroute = errorMessage;
await resultRepository.save(result);
}
});
// 收集输出
childProcess.stdout.on('data', (data: Buffer) => {
output += data.toString();
@ -488,7 +630,11 @@ class MonitoringService {
// 更新数据库中的结果
const result = await resultRepository.findOne({ where: { id: resultId } });
if (result) {
result.traceroute = `无法执行 Traceroute: ${err.message}`;
let errorMessage = `Traceroute 启动失败: ${err.message}`;
if (err.code === 'ENOENT') {
errorMessage = 'Traceroute 命令不存在,请确保系统已安装 traceroute 工具';
}
result.traceroute = errorMessage;
await resultRepository.save(result);
}

View File

@ -0,0 +1,158 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs';
import { AppDataSource } from '../index';
const execPromise = promisify(exec);
interface SystemDependency {
name: string;
command: string;
description: string;
required: boolean;
}
interface SystemHealth {
overall: 'healthy' | 'warning' | 'critical';
dependencies: DependencyStatus[];
resources: ResourceStatus;
database: DatabaseStatus;
timestamp: Date;
}
interface DependencyStatus {
name: string;
available: boolean;
version?: string;
error?: string;
required: boolean;
}
interface ResourceStatus {
diskSpace: string;
memoryUsage: string;
uptime: string;
}
interface DatabaseStatus {
connected: boolean;
error?: string;
}
class SystemHealthService {
private dependencies: SystemDependency[] = [
{ name: 'traceroute', command: 'traceroute --version', description: 'Linux路由追踪工具', required: true },
{ name: 'ping', command: 'ping -c 1 127.0.0.1', description: '网络连通性测试工具', required: true },
{ name: 'curl', command: 'curl --version', description: 'HTTP客户端工具', required: false },
{ name: 'nslookup', command: 'nslookup localhost', description: 'DNS查询工具', required: false }
];
async checkSystemHealth(): Promise<SystemHealth> {
const dependencyStatuses = await this.checkDependencies();
const resources = await this.checkResources();
const database = await this.checkDatabase();
const overall = this.calculateOverallHealth(dependencyStatuses, database);
return {
overall,
dependencies: dependencyStatuses,
resources,
database,
timestamp: new Date()
};
}
private async checkDependencies(): Promise<DependencyStatus[]> {
const results: DependencyStatus[] = [];
for (const dep of this.dependencies) {
try {
const { stdout } = await execPromise(dep.command);
results.push({
name: dep.name,
available: true,
version: stdout.trim().split('\n')[0],
required: dep.required
});
} catch (error: any) {
results.push({
name: dep.name,
available: false,
error: error.message,
required: dep.required
});
}
}
return results;
}
private async checkResources(): Promise<ResourceStatus> {
try {
const memUsage = process.memoryUsage();
const uptime = process.uptime();
return {
diskSpace: '检查中...',
memoryUsage: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB / ${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`,
uptime: `${Math.floor(uptime / 3600)}小时${Math.floor((uptime % 3600) / 60)}分钟`
};
} catch (error) {
return {
diskSpace: '无法检测',
memoryUsage: '无法检测',
uptime: '无法检测'
};
}
}
private async checkDatabase(): Promise<DatabaseStatus> {
try {
if (AppDataSource.isInitialized) {
await AppDataSource.query('SELECT 1');
return { connected: true };
} else {
return { connected: false, error: '数据库未初始化' };
}
} catch (error: any) {
return { connected: false, error: error.message };
}
}
private calculateOverallHealth(deps: DependencyStatus[], db: DatabaseStatus): 'healthy' | 'warning' | 'critical' {
if (!db.connected) return 'critical';
const requiredMissing = deps.filter(d => d.required && !d.available);
if (requiredMissing.length > 0) return 'critical';
const optionalMissing = deps.filter(d => !d.required && !d.available);
if (optionalMissing.length > 0) return 'warning';
return 'healthy';
}
getInstallInstructions(dependency: string): string {
const instructions: any = {
traceroute: {
ubuntu: 'sudo apt-get install traceroute',
centos: 'sudo yum install traceroute',
alpine: 'apk add traceroute'
},
ping: {
ubuntu: 'sudo apt-get install iputils-ping',
centos: 'sudo yum install iputils',
alpine: 'apk add iputils'
},
curl: {
ubuntu: 'sudo apt-get install curl',
centos: 'sudo yum install curl',
alpine: 'apk add curl'
}
};
return JSON.stringify(instructions[dependency] || {});
}
}
export const systemHealthService = new SystemHealthService();

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<title>Pingping - 网络拨测工具</title>
</head>
<body>
<div id="app"></div>

View File

@ -4,9 +4,13 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "vite --host 0.0.0.0 --port 3000",
"start:bg": "nohup vite --host 0.0.0.0 --port 3000 > logs/frontend.log 2>&1 &",
"stop": "pkill -f 'vite'",
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
"preview": "vite preview",
"logs": "tail -f logs/frontend.log"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",

View File

@ -14,12 +14,18 @@ importers:
axios:
specifier: ^1.8.4
version: 1.8.4
echarts:
specifier: ^5.6.0
version: 5.6.0
element-plus:
specifier: ^2.9.7
version: 2.9.7(vue@3.5.13(typescript@5.7.3))
vue:
specifier: ^3.5.13
version: 3.5.13(typescript@5.7.3)
vue-echarts:
specifier: ^7.0.3
version: 7.0.3(@vue/runtime-core@3.5.13)(echarts@5.6.0)(vue@3.5.13(typescript@5.7.3))
vue-router:
specifier: ^4.5.0
version: 4.5.0(vue@3.5.13(typescript@5.7.3))
@ -481,56 +487,67 @@ packages:
resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.40.0':
resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.40.0':
resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.40.0':
resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.40.0':
resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.40.0':
resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.40.0':
resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.40.0':
resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.40.0':
resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.40.0':
resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.40.0':
resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
@ -837,6 +854,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
echarts@5.6.0:
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
electron-to-chromium@1.5.136:
resolution: {integrity: sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==}
@ -1330,6 +1350,9 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@ -1397,6 +1420,17 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-demi@0.13.11:
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
@ -1408,6 +1442,16 @@ packages:
'@vue/composition-api':
optional: true
vue-echarts@7.0.3:
resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==}
peerDependencies:
'@vue/runtime-core': ^3.0.0
echarts: ^5.5.1
vue: ^2.7.0 || ^3.1.1
peerDependenciesMeta:
'@vue/runtime-core':
optional: true
vue-eslint-parser@10.1.3:
resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1453,6 +1497,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
snapshots:
'@ampproject/remapping@2.3.0':
@ -2227,6 +2274,11 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
echarts@5.6.0:
dependencies:
tslib: 2.3.0
zrender: 5.6.1
electron-to-chromium@1.5.136: {}
element-plus@2.9.7(vue@3.5.13(typescript@5.7.3)):
@ -2728,6 +2780,8 @@ snapshots:
dependencies:
typescript: 5.7.3
tslib@2.3.0: {}
tslib@2.8.1: {}
type-check@0.4.0:
@ -2758,10 +2812,24 @@ snapshots:
vscode-uri@3.1.0: {}
vue-demi@0.13.11(vue@3.5.13(typescript@5.7.3)):
dependencies:
vue: 3.5.13(typescript@5.7.3)
vue-demi@0.14.10(vue@3.5.13(typescript@5.7.3)):
dependencies:
vue: 3.5.13(typescript@5.7.3)
vue-echarts@7.0.3(@vue/runtime-core@3.5.13)(echarts@5.6.0)(vue@3.5.13(typescript@5.7.3)):
dependencies:
echarts: 5.6.0
vue: 3.5.13(typescript@5.7.3)
vue-demi: 0.13.11(vue@3.5.13(typescript@5.7.3))
optionalDependencies:
'@vue/runtime-core': 3.5.13
transitivePeerDependencies:
- '@vue/composition-api'
vue-eslint-parser@10.1.3(eslint@9.24.0):
dependencies:
debug: 4.4.0
@ -2807,3 +2875,7 @@ snapshots:
yallist@3.1.1: {}
yocto-queue@0.1.0: {}
zrender@5.6.1:
dependencies:
tslib: 2.3.0

View File

@ -54,6 +54,7 @@
</div>
</el-header>
<el-main>
<SystemAlert />
<router-view />
</el-main>
</el-container>
@ -83,6 +84,7 @@ import {
Moon,
Sunny,
} from '@element-plus/icons-vue';
import SystemAlert from './components/SystemAlert.vue';
const route = useRoute();
const isDarkMode = ref(localStorage.getItem('theme') === 'dark');

View File

@ -0,0 +1,81 @@
<template>
<el-alert
v-if="showAlert"
:title="alertTitle"
:description="alertDescription"
:type="alertType"
show-icon
:closable="false"
class="system-alert"
>
<template #default>
<div class="alert-actions">
<el-button size="small" @click="goToSettings">查看详情</el-button>
<el-button size="small" @click="dismissAlert">暂时忽略</el-button>
</div>
</template>
</el-alert>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
const showAlert = ref(false);
const alertTitle = ref('');
const alertDescription = ref('');
const alertType = ref<'warning' | 'error'>('warning');
const checkSystemHealth = async () => {
try {
const response = await axios.get('/api/system/health');
const health = response.data.data;
if (health.overall === 'critical') {
showAlert.value = true;
alertTitle.value = '系统状态异常';
alertDescription.value = '检测到关键系统依赖缺失,可能影响监控功能正常运行';
alertType.value = 'error';
} else if (health.overall === 'warning') {
showAlert.value = true;
alertTitle.value = '系统状态警告';
alertDescription.value = '部分系统工具不可用,建议安装以获得完整功能';
alertType.value = 'warning';
}
} catch (error) {
//
}
};
const goToSettings = () => {
router.push('/settings');
};
const dismissAlert = () => {
showAlert.value = false;
//
localStorage.setItem('systemAlert_dismissed', Date.now().toString());
};
onMounted(() => {
// 24
const dismissed = localStorage.getItem('systemAlert_dismissed');
if (!dismissed || Date.now() - parseInt(dismissed) > 24 * 60 * 60 * 1000) {
setTimeout(checkSystemHealth, 2000); // 2
}
});
</script>
<style scoped>
.system-alert {
margin-bottom: 16px;
}
.alert-actions {
margin-top: 8px;
display: flex;
gap: 8px;
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<el-card class="system-status-card" shadow="hover">
<template #header>
<div class="card-header">
<span>系统状态</span>
<el-button size="small" @click="refreshStatus" :loading="loading">刷新</el-button>
</div>
</template>
<div class="status-overview">
<el-tag
:type="overallStatusType"
size="large"
effect="dark"
class="overall-status"
>
{{ overallStatusText }}
</el-tag>
<div class="status-time">
最后检查: {{ statusTime }}
</div>
</div>
<el-divider />
<div class="dependency-list">
<h4>系统依赖</h4>
<el-row :gutter="16">
<el-col :span="12" v-for="dep in dependencies" :key="dep.name">
<div class="dependency-item">
<div class="dep-header">
<el-icon :color="dep.available ? '#67C23A' : '#F56C6C'">
<component :is="dep.available ? Check : Close" />
</el-icon>
<span class="dep-name">{{ dep.name }}</span>
<el-tag v-if="dep.required" size="small" type="danger" effect="plain">必需</el-tag>
</div>
<div v-if="!dep.available && dep.error" class="dep-error">
{{ dep.error }}
</div>
<div v-if="!dep.available" class="dep-install">
<el-button size="small" @click="showInstallInstructions(dep.name)">
查看安装方法
</el-button>
</div>
</div>
</el-col>
</el-row>
</div>
<el-divider />
<div class="resource-status">
<h4>系统资源</h4>
<el-row :gutter="16">
<el-col :span="8">
<div class="resource-item">
<span class="resource-label">内存使用:</span>
<span class="resource-value">{{ systemHealth?.resources?.memoryUsage || '检查中...' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="resource-item">
<span class="resource-label">运行时间:</span>
<span class="resource-value">{{ systemHealth?.resources?.uptime || '检查中...' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="resource-item">
<span class="resource-label">数据库:</span>
<el-tag :type="systemHealth?.database?.connected ? 'success' : 'danger'" size="small">
{{ systemHealth?.database?.connected ? '已连接' : '未连接' }}
</el-tag>
</div>
</el-col>
</el-row>
</div>
<!-- 安装说明对话框 -->
<el-dialog v-model="installDialogVisible" title="安装说明" width="500px">
<div v-if="installInstructions">
<p>请根据您的操作系统选择合适的安装命令</p>
<div v-for="(cmd, os) in installInstructions" :key="os" class="install-cmd">
<strong>{{ os.charAt(0).toUpperCase() + os.slice(1) }}:</strong>
<el-input :model-value="cmd" readonly class="cmd-input">
<template #append>
<el-button @click="copyCommand(cmd)">复制</el-button>
</template>
</el-input>
</div>
</div>
</el-dialog>
</el-card>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Check, Close } from '@element-plus/icons-vue';
import axios from 'axios';
//
const systemHealth = ref<any>(null);
const loading = ref(false);
const installDialogVisible = ref(false);
const installInstructions = ref<any>(null);
//
const overallStatusType = computed(() => {
if (!systemHealth.value) return 'info';
switch (systemHealth.value.overall) {
case 'healthy': return 'success';
case 'warning': return 'warning';
case 'critical': return 'danger';
default: return 'info';
}
});
const overallStatusText = computed(() => {
if (!systemHealth.value) return '检查中...';
switch (systemHealth.value.overall) {
case 'healthy': return '健康';
case 'warning': return '警告';
case 'critical': return '严重';
default: return '未知';
}
});
const dependencies = computed(() => systemHealth.value?.dependencies || []);
const statusTime = computed(() =>
systemHealth.value?.timestamp
? new Date(systemHealth.value.timestamp).toLocaleString()
: ''
);
//
const refreshStatus = async () => {
loading.value = true;
try {
const response = await axios.get('/api/system/health');
systemHealth.value = response.data.data;
} catch (error) {
ElMessage.error('获取系统状态失败');
} finally {
loading.value = false;
}
};
const showInstallInstructions = async (dependency: string) => {
try {
const response = await axios.get(`/api/system/install/${dependency}`);
installInstructions.value = response.data.data;
installDialogVisible.value = true;
} catch (error) {
ElMessage.error('获取安装说明失败');
}
};
const copyCommand = (command: string) => {
navigator.clipboard.writeText(command);
ElMessage.success('命令已复制到剪贴板');
};
onMounted(() => {
refreshStatus();
});
</script>
<style scoped>
.system-status-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.status-overview {
text-align: center;
margin-bottom: 20px;
}
.overall-status {
font-size: 16px;
font-weight: bold;
}
.status-time {
color: #909399;
font-size: 12px;
margin-top: 8px;
}
.dependency-item {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
}
.dep-header {
display: flex;
align-items: center;
gap: 8px;
}
.dep-name {
font-weight: 500;
flex: 1;
}
.dep-error {
color: #f56c6c;
font-size: 12px;
margin-top: 4px;
}
.dep-install {
margin-top: 8px;
}
.resource-item {
padding: 8px;
background-color: #f8f9fa;
border-radius: 4px;
margin-bottom: 8px;
}
.resource-label {
font-size: 12px;
color: #909399;
display: block;
}
.resource-value {
font-weight: 500;
color: #303133;
}
.install-cmd {
margin-bottom: 12px;
}
.cmd-input {
margin-top: 4px;
}
</style>

View File

@ -6,6 +6,9 @@
</div>
<el-tabs v-model="activeTab" class="settings-tabs">
<el-tab-pane label="系统状态" name="system">
<SystemStatus />
</el-tab-pane>
<el-tab-pane label="邮件服务器" name="email">
<el-card class="settings-card" shadow="hover">
<template #header>
@ -201,9 +204,10 @@
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import axios from 'axios';
import SystemStatus from '../components/SystemStatus.vue';
//
const activeTab = ref('email');
const activeTab = ref('system');
//
const emailLoading = ref(false);

View File

@ -7,12 +7,14 @@ export default defineConfig({
plugins: [vue(), vueJsx()],
server: {
port: 3000,
host: true,
proxy: {
'/api': {
target: 'http://localhost:2070/api',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
allowedHosts: ['autops.eagle.local']
}
})

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "pingping",
"version": "1.0.0",
"description": "Pingping - 网络拨测工具",
"private": true,
"workspaces": [
"backend",
"frontend"
],
"scripts": {
"start": "./start.sh",
"stop": "./stop.sh",
"dev": "concurrently \"pnpm --filter backend run dev\" \"pnpm --filter frontend run dev\"",
"build": "pnpm --filter backend run build && pnpm --filter frontend run build",
"start:backend": "pnpm --filter backend run start:daemon",
"start:frontend": "pnpm --filter frontend run start:bg",
"stop:backend": "pnpm --filter backend run stop",
"stop:frontend": "pnpm --filter frontend run stop",
"logs:backend": "pnpm --filter backend run logs",
"logs:frontend": "pnpm --filter frontend run logs",
"install:all": "pnpm install && pnpm --filter backend install && pnpm --filter frontend install"
},
"devDependencies": {
"concurrently": "^8.2.2"
},
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/your-username/pingping.git"
},
"keywords": [
"monitoring",
"network",
"ping",
"traceroute",
"health-check"
],
"author": "Your Name",
"license": "MIT"
}

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
packages:
- 'backend'
- 'frontend'

61
start.sh Executable file
View File

@ -0,0 +1,61 @@
#!/bin/bash
# Pingping 服务启动脚本
echo "🚀 正在启动 Pingping 服务..."
# 检查并停止现有服务
echo "📋 检查现有服务..."
pkill -f 'node.*dist/index.js' 2>/dev/null
pkill -f 'vite' 2>/dev/null
sleep 2
# 启动后端服务
echo "🔧 启动后端服务..."
cd backend
pnpm run build
pnpm run start:daemon
cd ..
# 等待后端启动
echo "⏳ 等待后端服务启动..."
sleep 5
# 检查后端是否启动成功
if curl -s http://localhost:2070/api/system/health > /dev/null; then
echo "✅ 后端服务启动成功 (http://localhost:2070)"
else
echo "❌ 后端服务启动失败"
exit 1
fi
# 启动前端服务
echo "🎨 启动前端服务..."
cd frontend
pnpm run start:bg
cd ..
# 等待前端启动
echo "⏳ 等待前端服务启动..."
sleep 5
# 检查前端是否启动成功
if curl -s http://localhost:3000 > /dev/null; then
echo "✅ 前端服务启动成功 (http://localhost:3000)"
else
echo "❌ 前端服务启动失败"
fi
echo "🎉 Pingping 服务启动完成!"
echo ""
echo "📊 服务地址:"
echo " 前端: http://localhost:3000"
echo " 后端: http://localhost:2070"
echo ""
echo "📝 查看日志:"
echo " 后端日志: pnpm --filter backend run logs"
echo " 前端日志: pnpm --filter frontend run logs"
echo ""
echo "🛑 停止服务:"
echo " 停止后端: pnpm --filter backend run stop"
echo " 停止前端: pnpm --filter frontend run stop"
echo " 停止全部: ./stop.sh"

23
stop.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
# Pingping 服务停止脚本
echo "🛑 正在停止 Pingping 服务..."
# 停止后端服务
echo "🔧 停止后端服务..."
pkill -f 'node.*dist/index.js' 2>/dev/null
# 停止前端服务
echo "🎨 停止前端服务..."
pkill -f 'vite' 2>/dev/null
sleep 2
# 检查是否完全停止
if pgrep -f 'node.*dist/index.js' > /dev/null || pgrep -f 'vite' > /dev/null; then
echo "⚠️ 部分服务可能仍在运行,请手动检查"
echo " 检查后端: ps aux | grep 'node.*dist/index.js'"
echo " 检查前端: ps aux | grep 'vite'"
else
echo "✅ 所有 Pingping 服务已停止"
fi