feat: 加入健康检查以及 pnpm start
This commit is contained in:
parent
2a84528345
commit
8f2711f728
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -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": [],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
packages:
|
||||
- 'backend'
|
||||
- 'frontend'
|
|
@ -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"
|
|
@ -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
|
Loading…
Reference in New Issue