Compare commits

..

9 Commits
dev ... master

Author SHA1 Message Date
niyyzf db3cb49989 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	web/.env
2024-08-03 14:32:11 +08:00
niyyzf c90f480fab 修改默认env 2024-08-03 14:31:54 +08:00
niyyzf a0ad936e17 修改默认env 2024-08-03 14:24:05 +08:00
niyyzf 9bcfd58062 调整跨域选项 2024-08-03 13:43:39 +08:00
niyyzf c84f3f6f0e 切换编译为传统的webpack 2024-08-03 13:15:41 +08:00
niyyzf 6d41883288 使用不兼容的 envs 2024-08-03 12:50:38 +08:00
niyyzf b66ead82fb Merge remote-tracking branch 'origin/master'
# Conflicts:
#	web/package.json
#	web/yarn.lock
2024-08-03 12:44:18 +08:00
zwb a93d42b61b 擅长不必要的库,减少体积,去除nuxt-image 2024-08-03 12:43:48 +08:00
zwb 17e18a5754 擅长不必要的库,减少体积 2024-08-03 11:27:39 +08:00
58 changed files with 2788 additions and 3029 deletions

View File

@ -1,4 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
using System.Text.Json;
using LoongPanel_Asp.Models;
using LoongPanel_Asp.utils; using LoongPanel_Asp.utils;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
@ -68,7 +71,7 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
).Metadata.SetValueComparer(new ListOfStringValueComparer()); ).Metadata.SetValueComparer(new ListOfStringValueComparer());
}); });
// 配置 ServerMonitoringData 实体 // 配置 ServerMonitoringData 实体
modelBuilder.Entity<ServerMonitoringData>(entity => modelBuilder.Entity<ServerMonitoringData>(entity =>
@ -112,14 +115,11 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
Name = roleName, Name = roleName,
NormalizedName = roleName.ToUpperInvariant(), NormalizedName = roleName.ToUpperInvariant(),
ApiPermissions = ApiPermissions =
[ ["1","2","3","4","5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35"],
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", RouterPermissions = ["1", "3", "4","5","6","7","8","9","10","11","12","13","14","15","16"]
"19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35"
],
RouterPermissions = ["1", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"]
}); });
} }
var apiRouterPermissions = ControllerScanner.GetApiPermissions(); var apiRouterPermissions = ControllerScanner.GetApiPermissions();
foreach (var permission in apiRouterPermissions) foreach (var permission in apiRouterPermissions)
@ -142,15 +142,16 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
new RotePermission { Id = 7, Name = "Gpu", Router = "^/host/gpu/.+$" }, new RotePermission { Id = 7, Name = "Gpu", Router = "^/host/gpu/.+$" },
new RotePermission { Id = 8, Name = "用户详细", Router = "^/userinfo/.+$" }, new RotePermission { Id = 8, Name = "用户详细", Router = "^/userinfo/.+$" },
new RotePermission { Id = 9, Name = "用户监测", Router = "^/serveruser/.+$" }, new RotePermission { Id = 9, Name = "用户监测", Router = "^/serveruser/.+$" },
new RotePermission { Id = 10, Name = "进程列表", Router = "/host/process" }, new RotePermission { Id = 10, Name = "进程列表", Router = "/host/process" },
new RotePermission { Id = 11, Name = "网络连接列表", Router = "/host/networklist" }, new RotePermission { Id = 11, Name = "网络连接列表", Router = "/host/networklist" },
new RotePermission { Id = 12, Name = "巡检记录", Router = "/inspectionrecords" }, new RotePermission { Id = 12, Name = "巡检记录", Router = "/inspectionrecords" },
new RotePermission { Id = 13, Name = "巡检记录", Router = "^/settings/.+$" }, new RotePermission { Id = 13, Name = "巡检记录", Router = "^/settings/.+$" },
new RotePermission { Id = 14, Name = "登录页面修改密码", Router = "^/changepassword/.+$" } new RotePermission { Id = 14, Name = "登录页面修改密码", Router = "^/changepassword/.+$" },
]; ];
foreach (var permission in rotePermissions) modelBuilder.Entity<RotePermission>().HasData(permission); foreach (var permission in rotePermissions) modelBuilder.Entity<RotePermission>().HasData(permission);
} }
} }
public class ApplicationUser : IdentityUser public class ApplicationUser : IdentityUser
@ -160,20 +161,22 @@ public class ApplicationUser : IdentityUser
[MaxLength(255)] public required string Avatar { get; set; } [MaxLength(255)] public required string Avatar { get; set; }
[MaxLength(255)] public string? Desc { get; set; } [MaxLength(255)] public string? Desc { get; set; }
[MaxLength(255)] public required string Posts { get; set; } [MaxLength(255)] public required string Posts { get; set; }
[MaxLength(255)] public string? Address { get; set; } [MaxLength(255)] public string? Address { get; set; }
[MaxLength(255)] public DateTime? LastLoginTime { get; set; } [MaxLength(255)] public DateTime? LastLoginTime { get; set; }
[MaxLength(255)] public required DateTime CreateDate { get; init; } [MaxLength(255)] public required DateTime CreateDate { get; init; }
[MaxLength(255)] public required DateTime ModifiedDate { get; set; } [MaxLength(255)] public required DateTime ModifiedDate { get; set; }
[MaxLength(255)] public string? PhysicalAddress { get; set; } [MaxLength(255)] public string? PhysicalAddress { get; set; }
[DataType(DataType.DateTime)] public DateTimeOffset? PasswordExpiredDate { get; set; } [DataType(DataType.DateTime)]
public DateTimeOffset? PasswordExpiredDate { get; set; }
} }
public class ApplicationRole : IdentityRole public class ApplicationRole : IdentityRole
@ -206,13 +209,13 @@ public class ServerMonitoringData
public DateTime? Time { get; set; } public DateTime? Time { get; set; }
public required string? ServerId { get; set; } public required string ServerId { get; set; }
public required string? Data { get; set; } public required string? Data { get; set; }
public required string DataName { get; set; } public required string DataName { get; set; }
public required string DataType { get; set; } public required string? DataType { get; set; }
} }
public class ListOfStringValueComparer() : ValueComparer<List<string>>((c1, c2) => c1!.SequenceEqual(c2!), public class ListOfStringValueComparer() : ValueComparer<List<string>>((c1, c2) => c1!.SequenceEqual(c2!),

View File

@ -1,5 +1,5 @@
[CpuTotalUsage_d3YT] [CpuTotalUsage_d3YT]
ValueName = Cpu总使用率 ValueName=Cpu总使用率
Description = Cpu总使用率超过20%通知,超过30%警告 Description=Cpu总使用率超过20%通知,超过30%警告
Notify = 20 Notify=20
Warning = 30 Warning=30

View File

@ -1,11 +1,4 @@
[DataJob] [CpuTotalJob]
Group = Data
ValueName = 数据库提交
Description = A simple job that uses the database
JobType = LoongPanel_Asp.Jobs.DataJob, LoongPanel-Asp
CronExpression = 0 0/1 * * * ? *
[CpuTotalJob]
Group = CPU Group = CPU
ValueName = CPU使用率 ValueName = CPU使用率
Description = A simple job that uses the CPU Description = A simple job that uses the CPU
@ -29,15 +22,6 @@ JobType = LoongPanel_Asp.Jobs.CpuSpeedJob, LoongPanel-Asp
Executor = d3YT,xseg Executor = d3YT,xseg
CronExpression = 2/10 * * * * ? * CronExpression = 2/10 * * * * ? *
[SystemTimeIntervalLoadJob]
Group = System
ValueName = 系统间隔负载
Description = A simple job that uses the CPU
JobType = LoongPanel_Asp.Jobs.SystemTimeIntervalLoadJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 1/5 * * * * ? *
[ProcessTotalJob] [ProcessTotalJob]
Group = Process Group = Process
ValueName = 进程总使用数 ValueName = 进程总使用数

View File

@ -28,7 +28,7 @@ public class AccountController(
//判断用户名,邮箱是否唯一 //判断用户名,邮箱是否唯一
var user = await userManager.FindByNameAsync(model.UserName); var user = await userManager.FindByNameAsync(model.UserName);
if (user != null) return BadRequest("用户名已存在"); if (user != null) return BadRequest("用户名已存在");
//创建用户 //创建用户
user = new ApplicationUser user = new ApplicationUser
{ {
@ -39,16 +39,14 @@ public class AccountController(
Email = model.Email, Email = model.Email,
UserName = model.UserName, UserName = model.UserName,
PhoneNumber = model.Phone, PhoneNumber = model.Phone,
NickName = model.FullName NickName = model.FullName,
}; };
var result = await userManager.CreateAsync(user, model.Password); var result = await userManager.CreateAsync(user, model.Password);
if (!result.Succeeded) if (!result.Succeeded) return BadRequest("无法创建用户,"+string.Join(",",result.Errors.ToList().Select(e=>e.Description)));
return BadRequest("无法创建用户," + string.Join(",", result.Errors.ToList().Select(e => e.Description)));
//添加用户到默认角色 //添加用户到默认角色
result = await userManager.AddToRoleAsync(user, model.Role); result = await userManager.AddToRoleAsync(user, model.Role);
if (!result.Succeeded) if (!result.Succeeded) return BadRequest("无法创建用户,"+string.Join(",",result.Errors.ToList().Select(e=>e.Description)));
return BadRequest("无法创建用户," + string.Join(",", result.Errors.ToList().Select(e => e.Description)));
return Ok("用户创建成功"); return Ok("用户创建成功");
} }
catch (Exception e) catch (Exception e)
@ -57,7 +55,7 @@ public class AccountController(
throw; throw;
} }
} }
[HttpPost("Login")] [HttpPost("Login")]
public async Task<IActionResult> Login([FromBody] LoginModel model) public async Task<IActionResult> Login([FromBody] LoginModel model)
@ -69,25 +67,31 @@ public class AccountController(
else else
user = await userManager.FindByNameAsync(model.EmailOrUserName); user = await userManager.FindByNameAsync(model.EmailOrUserName);
if (user == null) return BadRequest("用户不存在"); if (user == null) return BadRequest("用户不存在");
var result = await signInManager.CheckPasswordSignInAsync(user, model.Password, true); var result = await signInManager.CheckPasswordSignInAsync(user, model.Password,true);
if (!result.Succeeded) return BadRequest("错误账号或密码"); if (!result.Succeeded) return BadRequest("错误账号或密码");
if (await userManager.IsLockedOutAsync(user)) return BadRequest("账号已锁定,请联系管理员"); if (await userManager.IsLockedOutAsync(user))
if (!user.EmailConfirmed && user.UserName != "admin") {
return BadRequest("账号已锁定,请联系管理员");
}
if (!user.EmailConfirmed&&user.UserName!="admin")
{
return Unauthorized(new return Unauthorized(new
{ {
user.Email, user.Email,
user.Id user.Id
}); });
}
//创建修改密码令牌 //创建修改密码令牌
var tokenPassword = await userManager.GeneratePasswordResetTokenAsync(user); var tokenPassword = await userManager.GeneratePasswordResetTokenAsync(user);
if ((user.PasswordExpiredDate == null || user.PasswordExpiredDate < DateTimeOffset.Now) && if ((user.PasswordExpiredDate == null || user.PasswordExpiredDate < DateTimeOffset.Now)&&user.UserName!="admin")
user.UserName != "admin") {
//返回402 //返回402
return StatusCode(402, new return StatusCode(402,new
{ {
tokenPassword, tokenPassword,
user.Id user.Id,
}); });
}
var roles = await userManager.GetRolesAsync(user); var roles = await userManager.GetRolesAsync(user);
var roleId = roles.ToList()[0]; // 直接获取角色ID列表 var roleId = roles.ToList()[0]; // 直接获取角色ID列表
var claimsIdentity = new ClaimsIdentity(new[] var claimsIdentity = new ClaimsIdentity(new[]
@ -97,22 +101,21 @@ public class AccountController(
new Claim(ClaimTypes.Role, roleId.ToLower()) // 将角色ID列表转换为逗号分隔的字符串 new Claim(ClaimTypes.Role, roleId.ToLower()) // 将角色ID列表转换为逗号分隔的字符串
}); });
var token = tokenHelper.GenerateToken(claimsIdentity); var token = tokenHelper.GenerateToken(claimsIdentity);
return Ok(new return Ok(new
{ {
user.NickName, user.NickName,
Token = token Token=token
}); });
} }
[HttpGet("VerifyEmail")] [HttpGet("VerifyEmail")]
public async Task<IActionResult> VerifyEmail([FromQuery] string userId, [FromQuery] string email, public async Task<IActionResult> VerifyEmail( [FromQuery] string userId,[FromQuery] string email ,[FromQuery] string? code = null)
[FromQuery] string? code = null)
{ {
//如果code 不为空 //如果code 不为空
if (code != null) if (code != null)
{ {
var r = emailService.VerifyEmailVerifyCode(userId, code); var r= emailService.VerifyEmailVerifyCode(userId,code);
if (!r) return BadRequest("验证码错误"); if (!r) return BadRequest("验证码错误");
//验证成功 //验证成功
//更新用户信息 //更新用户信息
@ -123,28 +126,31 @@ public class AccountController(
await userManager.UpdateAsync(user); await userManager.UpdateAsync(user);
return Ok("邮箱验证成功"); return Ok("邮箱验证成功");
} }
await emailService.SendEmailVerifyCodeAsync( userId,email, "尊敬的用户");
await emailService.SendEmailVerifyCodeAsync(userId, email, "尊敬的用户");
return Ok("邮件已发送"); return Ok("邮件已发送");
} }
[HttpGet("ChangePassword")] [HttpGet("ChangePassword")]
public async Task<IActionResult> ChangePassword([FromQuery] string userId, [FromQuery] string token, public async Task<IActionResult> ChangePassword([FromQuery] string userId, [FromQuery] string token,[FromQuery] string newPassword)
[FromQuery] string newPassword)
{ {
// 获取当前经过身份验证的用户 // 获取当前经过身份验证的用户
var authenticatedUser = await userManager.FindByIdAsync(userId); var authenticatedUser = await userManager.FindByIdAsync(userId);
if (authenticatedUser == null) return BadRequest("用户不存在"); if (authenticatedUser == null)
{
return BadRequest("用户不存在");
}
// 重置密码 // 重置密码
var result = await userManager.ResetPasswordAsync(authenticatedUser, token, newPassword); var result = await userManager.ResetPasswordAsync(authenticatedUser, token, newPassword);
if (!result.Succeeded) return BadRequest("修改密码失败"); if (!result.Succeeded)
{
return BadRequest("修改密码失败");
}
//修改用户的过期时间为1天后 //修改用户的过期时间为1天后
authenticatedUser.PasswordExpiredDate = DateTimeOffset.Now.AddDays(1); authenticatedUser.PasswordExpiredDate = DateTimeOffset.Now.AddDays(1);
await userManager.UpdateAsync(authenticatedUser); await userManager.UpdateAsync(authenticatedUser);
return Ok("修改密码成功"); return Ok("修改密码成功");
} }
[HttpGet("ForgotPassword")] [HttpGet("ForgotPassword")]
public async Task<IActionResult> ForgotPassword([FromQuery] string email) public async Task<IActionResult> ForgotPassword([FromQuery] string email)
{ {
@ -162,7 +168,7 @@ public class AccountController(
var user = await userManager.FindByEmailAsync(email); var user = await userManager.FindByEmailAsync(email);
if (user == null) return BadRequest("用户不存在"); if (user == null) return BadRequest("用户不存在");
//随机生成密码12位 特殊字符大小写数字 英文 //随机生成密码12位 特殊字符大小写数字 英文
token = token.Replace(" ", "+"); token= token.Replace(" ", "+");
var newPassword = GenerateRandomPassword(32); var newPassword = GenerateRandomPassword(32);
var result = await userManager.ResetPasswordAsync(user, token, newPassword); var result = await userManager.ResetPasswordAsync(user, token, newPassword);
if (!result.Succeeded) return BadRequest(result.Errors.FirstOrDefault()?.Description); if (!result.Succeeded) return BadRequest(result.Errors.FirstOrDefault()?.Description);
@ -173,7 +179,7 @@ public class AccountController(
$"<h1>您的新密码是:<strong>{newPassword}</strong></h1>"); $"<h1>您的新密码是:<strong>{newPassword}</strong></h1>");
return Ok("密码重置成功,新密码已生成并发送给用户邮箱。"); return Ok("密码重置成功,新密码已生成并发送给用户邮箱。");
} }
//登出 //登出
[HttpGet("Logout")] [HttpGet("Logout")]
public async Task<IActionResult> Logout() public async Task<IActionResult> Logout()
@ -181,7 +187,6 @@ public class AccountController(
await signInManager.SignOutAsync(); await signInManager.SignOutAsync();
return Ok("Logout successful"); return Ok("Logout successful");
} }
private string GenerateRandomPassword(int length) private string GenerateRandomPassword(int length)
{ {
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*"; const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*";
@ -190,7 +195,6 @@ public class AccountController(
{ {
rng.GetBytes(byteBuffer); rng.GetBytes(byteBuffer);
} }
return new string(byteBuffer.Select(b => chars[b % chars.Length]).ToArray()); return new string(byteBuffer.Select(b => chars[b % chars.Length]).ToArray());
} }
} }

View File

@ -1,5 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using IniParser; using IniParser;
using IniParser.Model;
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -7,23 +8,22 @@ namespace LoongPanel_Asp.Controllers;
[ApiController] [ApiController]
[Route("Api/[controller]")] [Route("Api/[controller]")]
public class ConfigController : ControllerBase public class ConfigController:ControllerBase
{ {
[HttpGet("GetAlertConfig")] [HttpGet("GetAlertConfig")]
public IActionResult GetAlertConfig() public IActionResult GetAlertConfig()
{ {
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value; var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
//从Configs/Alerts/userId.ini //从Configs/Alerts/userId.ini
var parser = new FileIniDataParser(); var parser=new FileIniDataParser();
var fullPath = Path.Combine("Configs", "Alerts", $"{userId}.ini"); var fullPath=Path.Combine("Configs","Alerts",$"{userId}.ini");
//判断文件是否存在 //判断文件是否存在
if (!System.IO.File.Exists(fullPath)) if (!System.IO.File.Exists(fullPath))
{ {
//复制一份默认配置文件 //复制一份默认配置文件
System.IO.File.Copy(Path.Combine("Configs", "alert.ini"), fullPath); System.IO.File.Copy(Path.Combine("Configs","alert.ini"),fullPath);
JobConfigHelper.ReloadAlerts(); JobConfigHelper.ReloadAlerts();
} }
var data = parser.ReadFile(fullPath); var data = parser.ReadFile(fullPath);
//获得所有section //获得所有section
var sections = data.Sections; var sections = data.Sections;
@ -34,10 +34,10 @@ public class ConfigController : ControllerBase
{ {
AlertType = x.SectionName.Split("_")[0], AlertType = x.SectionName.Split("_")[0],
ServerId = x.SectionName.Split("_")[1], ServerId = x.SectionName.Split("_")[1],
Notify = x.Keys["Notify"], Notify= x.Keys["Notify"],
Warning = x.Keys["Warning"], Warning=x.Keys["Warning"],
TypeName = x.Keys["ValueName"], TypeName=x.Keys["ValueName"],
Description = x.Keys["Description"] Description=x.Keys["Description"],
}; };
return alertConfiguration; return alertConfiguration;
}); });
@ -48,32 +48,37 @@ public class ConfigController : ControllerBase
public IActionResult AddAlertConfig([FromBody] AlertModel model) public IActionResult AddAlertConfig([FromBody] AlertModel model)
{ {
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value; var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
var fullPath = Path.Combine("Configs", "Alerts", $"{userId}.ini"); var fullPath=Path.Combine("Configs","Alerts",$"{userId}.ini");
var parser = new FileIniDataParser(); var parser=new FileIniDataParser();
var data = parser.ReadFile(fullPath); var data = parser.ReadFile(fullPath);
//判断是否存在 //判断是否存在
if (data.Sections.Contains($"{model.DataType}_{model.ServerId}")) return BadRequest("配置已存在"); if (data.Sections.Contains($"{model.DataType}_{model.ServerId}"))
var sectionName = $"{model.DataType}_{model.ServerId}"; {
data[sectionName]["Notify"] = model.Notify; return BadRequest("配置已存在");
data[sectionName]["Warning"] = model.Warning; }
data[sectionName]["ValueName"] = model.DataName; var sectionName=$"{model.DataType}_{model.ServerId}";
data[sectionName]["Description"] = model.Description; data[sectionName]["Notify"]=model.Notify;
parser.WriteFile(fullPath, data); data[sectionName]["Warning"]=model.Warning;
data[sectionName]["ValueName"]=model.DataName;
data[sectionName]["Description"]=model.Description;
parser.WriteFile(fullPath,data);
JobConfigHelper.ReloadAlerts(); JobConfigHelper.ReloadAlerts();
return Ok("配置已添加"); return Ok("配置已添加");
} }
[HttpDelete("DeleteAlertConfig")] [HttpDelete("DeleteAlertConfig")]
public IActionResult DeleteAlertConfig([FromQuery] string dataType, [FromQuery] string serverId) public IActionResult DeleteAlertConfig([FromQuery] string dataType,[FromQuery] string serverId)
{ {
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value; var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
var fullPath = Path.Combine("Configs", "Alerts", $"{userId}.ini"); var fullPath=Path.Combine("Configs","Alerts",$"{userId}.ini");
var parser = new FileIniDataParser(); var parser=new FileIniDataParser();
var data = parser.ReadFile(fullPath); var data = parser.ReadFile(fullPath);
//判断是否存在 //判断是否存在
if (!data.Sections.Contains($"{dataType}_{serverId}")) return BadRequest("配置不存在"); if (!data.Sections.Contains($"{dataType}_{serverId}"))
{
return BadRequest("配置不存在");
}
data.Sections.RemoveSection($"{dataType}_{serverId}"); data.Sections.RemoveSection($"{dataType}_{serverId}");
parser.WriteFile(fullPath, data); parser.WriteFile(fullPath,data);
JobConfigHelper.ReloadAlerts(); JobConfigHelper.ReloadAlerts();
return Ok("配置已删除"); return Ok("配置已删除");
} }
@ -81,11 +86,11 @@ public class ConfigController : ControllerBase
public class AlertModel public class AlertModel
{ {
public required string ServerId { get; init; } public required string ServerId {get;init;}
public required string DataType { get; init; } public required string DataType {get;init;}
public required string Description { get; init; } public required string Description {get;init;}
public required string Notify { get; init; } public required string Notify {get;init;}
public required string Warning { get; init; } public required string Warning {get;init;}
public required string DataName { get; init; } public required string DataName {get;init;}
} }

View File

@ -8,34 +8,23 @@ namespace LoongPanel_Asp.Controllers;
public class JobController(ApplicationDbContext dbContext) : ControllerBase public class JobController(ApplicationDbContext dbContext) : ControllerBase
{ {
[HttpGet("GetJobList")] [HttpGet("GetJobList")]
public IActionResult GetJobList([FromQuery] string? serverId = null) public IActionResult GetJobList([FromQuery]string? serverId=null)
{ {
var wildcardRules = new Dictionary<string, List<string>> var wildcardRules = new Dictionary<string, List<string>>
{ {
{ "CPU总体参数", ["CpuTotalUsage", "CpuTotalSpeed", "CpuIOWaitUsage", "CpuSystemUsage", "CpuUserUsage"] }, { "CPU总体参数", ["CpuTotalUsage","CpuTotalSpeed","CpuIOWaitUsage","CpuSystemUsage","CpuUserUsage"] },
{ "CPU单核参数", ["CpuSingleSpeed-*"] }, { "CPU单核参数", ["CpuSingleSpeed-*"] },
{ "CPU用户参数", ["CpuUsage-*"] }, { "CPU用户参数", ["CpuUsage-*"] },
{ "内存总体参数", ["MemoryCache", "MemoryFree", "MemoryTotal", "MemoryTotalUsage", "MemoryUsed"] }, { "内存总体参数", ["MemoryCache","MemoryFree","MemoryTotal","MemoryTotalUsage","MemoryUsed"] },
{ "内存用户参数", ["MemoryUsage-*"] }, { "内存用户参数", ["MemoryUsage-*"] },
{ "Swap参数", ["SwapFree", "SwapTotal", "SwapTotalUsage", "SwapUsed"] }, { "Swap参数", ["SwapFree","SwapTotal","SwapTotalUsage","SwapUsed"] },
{ { "各个磁盘参数", ["DiskTps-*","DiskTps-*","DiskUtil-*","DiskWriteKB-*","DiskWriteKB-*","DiskAwait-*","DiskReadKB-*"] },
"各个磁盘参数",
[
"DiskTps-*", "DiskTps-*", "DiskUtil-*", "DiskWriteKB-*", "DiskWriteKB-*", "DiskAwait-*",
"DiskReadKB-*"
]
},
{ "磁盘总体参数", ["DiskTotalUsage"] }, { "磁盘总体参数", ["DiskTotalUsage"] },
{ "网络总体参数", ["InterfaceTotalUtilizationPercentage"] }, { "网络总体参数", ["InterfaceTotalUtilizationPercentage"] },
{ { "各个网络接口参数", ["InterfaceUtilizationPercentage-*","InterfaceUtilizationPercentage-*","ReceivedPacketsPerSecond-*","TransmittedPacketsPerSecond-*"] },
"各个网络接口参数", { "进程", ["PhrasePatternCount","ProcessTotalCount","ThreadsTotalCount"] },
[ { "用户进程", ["UserProcesses-*"] },
"InterfaceUtilizationPercentage-*", "InterfaceUtilizationPercentage-*",
"ReceivedPacketsPerSecond-*", "TransmittedPacketsPerSecond-*"
]
},
{ "进程", ["PhrasePatternCount", "ProcessTotalCount", "ThreadsTotalCount"] },
{ "用户进程", ["UserProcesses-*"] }
}; };
var filteredData = dbContext.ServerMonitoringData var filteredData = dbContext.ServerMonitoringData
.Where(x => string.IsNullOrEmpty(serverId) || x.ServerId == serverId) .Where(x => string.IsNullOrEmpty(serverId) || x.ServerId == serverId)
@ -47,22 +36,21 @@ public class JobController(ApplicationDbContext dbContext) : ControllerBase
{ {
x.DataName, x.DataName,
x.DataType, x.DataType,
GroupName = wildcardRules.FirstOrDefault(rule => x.DataType != null && IsMatch(x.DataType, rule.Value)) GroupName = wildcardRules.FirstOrDefault(rule => x.DataType != null && IsMatch(x.DataType, rule.Value)).Key ?? "其他"
.Key ?? "其他"
}) })
.GroupBy(x => x.GroupName) .GroupBy(x => x.GroupName)
.Select(group => new .Select(group => new
{ {
GroupName = group.Key, GroupName=group.Key,
Items = group.ToList() Items = group.ToList()
}) })
.ToList(); .ToList();
return Ok(groupedData); return Ok(groupedData);
} }
private bool IsMatch(string input, List<string> patterns) private bool IsMatch(string input, List<string> patterns)
{ {
return patterns.Select(pattern => "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$") return patterns.Select(pattern => "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$").Any(regexPattern => Regex.IsMatch(input, regexPattern));
.Any(regexPattern => Regex.IsMatch(input, regexPattern));
} }
} }

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
namespace LoongPanel_Asp.Controllers; namespace LoongPanel_Asp.Controllers;
@ -9,13 +10,22 @@ public class PublicFileController(IWebHostEnvironment webHostEnvironment) : Cont
[HttpPost("UploadImage")] [HttpPost("UploadImage")]
public async Task<IActionResult> UploadImage(IFormFile? file) public async Task<IActionResult> UploadImage(IFormFile? file)
{ {
if (file == null || file.Length == 0) return BadRequest("文件不能为空"); if (file == null || file.Length == 0)
{
return BadRequest("文件不能为空");
}
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" }; var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
Console.WriteLine(extension); Console.WriteLine(extension);
if (!allowedExtensions.Contains(extension)) return BadRequest("不支持的文件类型"); if (!allowedExtensions.Contains(extension))
{
return BadRequest("不支持的文件类型");
}
var uploadsFolderPath = Path.Combine(webHostEnvironment.WebRootPath, "public/image"); var uploadsFolderPath = Path.Combine(webHostEnvironment.WebRootPath, "public/image");
if (!Directory.Exists(uploadsFolderPath)) Directory.CreateDirectory(uploadsFolderPath); if (!Directory.Exists(uploadsFolderPath))
{
Directory.CreateDirectory(uploadsFolderPath);
}
var uniqueFileName = $"{Guid.NewGuid()}{extension}"; var uniqueFileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(uploadsFolderPath, uniqueFileName); var filePath = Path.Combine(uploadsFolderPath, uniqueFileName);

View File

@ -1,5 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using LoongPanel_Asp.Helpers;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -19,14 +20,16 @@ public class RoteController(
var role = await roleManager.FindByNameAsync(roleId); var role = await roleManager.FindByNameAsync(roleId);
var rotes = role!.RouterPermissions.ToList(); var rotes = role!.RouterPermissions.ToList();
//获取路由列表 //获取路由列表
var apiPermissions = dbContext.RotePermissions.ToList().Where(x => rotes.Any(y => y == x.Id.ToString())) var apiPermissions = dbContext.RotePermissions.ToList().Where(x => rotes.Any(y => y == x.Id.ToString())).Select(x=>x.Router).ToList();
.Select(x => x.Router).ToList();
//如果rotes 中包括* //如果rotes 中包括*
if (rotes.Contains("*")) return Ok("权限验证通过"); if (rotes.Contains("*"))
{
return Ok("权限验证通过");
}
//将path全部小写 //将path全部小写
path = path.ToLower(); path = path.ToLower();
//使用正则匹配 //使用正则匹配
var firstOrDefault = apiPermissions.FirstOrDefault(x => Regex.IsMatch(path, x)); var firstOrDefault = apiPermissions.FirstOrDefault(x => Regex.IsMatch(path,x));
return Ok(firstOrDefault != null ? Ok("权限验证通过") : Unauthorized("你不具有访问此资源的权力")); return Ok(firstOrDefault != null ? Ok("权限验证通过") : Unauthorized("你不具有访问此资源的权力"));
} }
} }

View File

@ -1,29 +1,29 @@
using System.Dynamic; using System.Collections.Concurrent;
using System.Collections.Specialized;
using System.Dynamic;
using System.Globalization; using System.Globalization;
using System.Security.Claims; using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs; using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers; using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace LoongPanel_Asp.Controllers; namespace LoongPanel_Asp.Controllers;
[ApiController] [ApiController]
[Route("Api/[controller]")] [Route("Api/[controller]")]
public class ServerController( public class ServerController(IServiceProvider serviceProvider, ApplicationDbContext dbContext) : ControllerBase
IServiceProvider serviceProvider,
ApplicationDbContext dbContext,
IHubContext<SessionHub> hubContext) : ControllerBase
{ {
//获取远程服务器地址列表 //获取远程服务器地址列表
[HttpGet("GetServerList")] [HttpGet("GetServerList")]
public IActionResult GetServerList() public IActionResult GetServerList()
{ {
var serverConfigs = JobConfigHelper.GetServers(); var serverConfigs=JobConfigHelper.GetServers();
//读取每一个配置项转换为列表 //读取每一个配置项转换为列表
var serverList = serverConfigs.Select(section => new ServerInfo var serverList = serverConfigs.Select(section => new ServerInfo
{ {
@ -51,36 +51,64 @@ public class ServerController(
public async Task<IActionResult> GetServerHistoryDate([FromQuery] string serverId, public async Task<IActionResult> GetServerHistoryDate([FromQuery] string serverId,
[FromQuery] List<string?>? dataTypes = null, [FromQuery] int? startIndex = 0) [FromQuery] List<string?>? dataTypes = null, [FromQuery] int? startIndex = 0)
{ {
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value; if (string.IsNullOrWhiteSpace(serverId)) return BadRequest("ServerId is required.");
//创建查询 流式
var query = dbContext.ServerMonitoringData
.AsNoTracking() // 不追踪实体状态
.Where(x => x.ServerId == serverId) // 筛选服务器ID
.Where(x => dataTypes == null || dataTypes.Contains(x.DataType)) // 如果dataTypes不为空则进一步筛选
.OrderBy(x => x.Time)
.Skip(Math.Max(startIndex ?? 0, 0)) // 跳过指定数量的记录
.Take(1000)
.AsAsyncEnumerable(); // 启用流式查询
Dictionary<string, List<List<object>>> temp = new();
await foreach (var data in query) var query = dbContext.ServerMonitoringData
.Where(s => s.ServerId == serverId && s.DataType != null);
if (dataTypes != null && dataTypes.Any()) query = query.Where(s => dataTypes.Contains(s.DataType));
var allData = await query
.OrderByDescending(s => s.Time)
.Skip(startIndex ?? 0)
.Take(1000)
.ToListAsync();
if (allData.Count == 0) return Ok(new { done = true });
// 获取时间集合并排序
var timeList = allData.Select(s => s.Time).Distinct().OrderBy(s => s).ToList();
// 获取DataType
var groupedData = new ConcurrentDictionary<string, List<double>>();
var distinctDataTypes = dataTypes ?? allData.Select(s => s.DataType).Distinct().ToList();
foreach (var dataType in distinctDataTypes)
{ {
if (!temp.TryGetValue(data.DataType, out var value)) var dataList = allData.Where(s => s.DataType == dataType).ToList();
var backDataList = new List<double>();
var temp = double.Parse(dataList[0].Data ?? string.Empty);
foreach (var time in timeList)
{ {
value = new List<List<object>>(); var currentData = dataList.Where(d => d.Time == time).ToList();
temp[data.DataType] = value; if (currentData.Count > 0)
{
//计算data平均值
var dataSum = currentData.Sum(d => double.Parse(d.Data ?? string.Empty));
temp = dataSum;
}
backDataList.Add(temp);
} }
var unixTimestamp = ((DateTimeOffset)data.Time?.ToUniversalTime()!).ToUnixTimeMilliseconds(); if (dataType != null) groupedData[dataType] = backDataList;
value.Add([unixTimestamp!, data.Data!]);
} }
foreach (var entry in temp) //timelist转换为当前时间 月 日 分 秒
{ var timeList2 = timeList.Select(time => time?.ToLocalTime().ToString("MM-dd HH:mm:ss")).ToList();
await hubContext.Clients.Groups(userId).SendAsync("ReceiveDataHistory", entry.Key, entry.Value, true);
} // 返回结果
return Ok(); var returnValue = new
{
times = timeList2,
data = groupedData,
endIndex = dataTypes is { Count: > 0 } ? startIndex + timeList2.Count * dataTypes.Count :
startIndex + allData.Count < 1000 ? allData.Count : 1000,
done = false
};
return Ok(returnValue);
} }
@ -93,16 +121,63 @@ public class ServerController(
return Ok(output); return Ok(output);
} }
[HttpGet("GetServerHistoryStep")]
public async Task<IActionResult> GetServerHistoryStep([FromQuery] string serverId, [FromQuery] string dataType,
[FromQuery] string timeRange)
{
var query = dbContext.ServerMonitoringData
.Where(s => s.ServerId == serverId && s.DataType != null && s.DataType == dataType);
int timeSpan;
int timeNum;
switch (timeRange.ToLower())
{
case "1m":
timeSpan = 1;
timeNum = 60;
break;
case "1h":
timeSpan = 60;
timeNum = 60;
break;
case "1d":
timeSpan = 60 * 12;
timeNum = 12;
break;
default:
return BadRequest("Invalid time range.");
}
var time = DateTime.UtcNow;
var timeList = new List<DateTime>();
var dataList = new List<string>();
for (var i = 0; i < timeNum; i++)
{
var endTime = time.AddSeconds(-(timeSpan * i) + 3 * timeSpan);
var startTime = time.AddSeconds(-(timeSpan * i) - 3 * timeSpan);
var data = query.FirstOrDefault(d => d.Time <= endTime && d.Time >= startTime);
var value = "0";
if (data is { Data: not null }) value = data.Data;
timeList.Add(startTime);
dataList.Add(value);
}
dataList.Reverse();
var backData = new
{
data = dataList,
time = timeList.Select(dateTime => dateTime.ToLocalTime().ToString("MM-dd HH:mm:ss")).ToList()
};
return Ok(backData);
}
[HttpGet("GetServerMemoryInfo")] [HttpGet("GetServerMemoryInfo")]
public async Task<IActionResult> GetServerMemoryInfo([FromQuery] string serverId) public async Task<IActionResult> GetServerMemoryInfo([FromQuery] string serverId)
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x =>x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(false, serverId, "echo", $"'{server.Password}'", "|", "sudo", var output = await sshClient?.ExecuteCommandAsync(false, serverId,"echo",$"'{server.Password}'","|","sudo","-S","lshw","-class","memory","-json")!;
"-S", "lshw", "-class", "memory", "-json")!;
if (string.IsNullOrEmpty(output)) return BadRequest(); if (string.IsNullOrEmpty(output)) return BadRequest();
return Ok(output); return Ok(output);
} }
@ -113,14 +188,12 @@ public class ServerController(
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x =>x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(serverId, $"echo {server.Password}", "|", var output = await sshClient?.ExecuteCommandAsync(serverId,$"echo {server.Password}","|","sudo -S /usr/sbin/fdisk -l","|","grep 'Disk /'","|","awk '{print $2,$3}'")!;
"sudo -S /usr/sbin/fdisk -l", "|", "grep 'Disk /'", "|", "awk '{print $2,$3}'")!;
if (string.IsNullOrEmpty(output)) return BadRequest(output); if (string.IsNullOrEmpty(output)) return BadRequest(output);
var diskList = output.Split("\n", StringSplitOptions.RemoveEmptyEntries); var diskList = output.Split("\n", StringSplitOptions.RemoveEmptyEntries);
var outList = diskList.Select(disk => disk.Split(":", StringSplitOptions.RemoveEmptyEntries)) var outList = diskList.Select(disk => disk.Split(":", StringSplitOptions.RemoveEmptyEntries)).Select(info => new { name = info[0], size = info[1] }).ToList();
.Select(info => new { name = info[0], size = info[1] }).ToList();
return Ok(outList); return Ok(outList);
} }
@ -129,49 +202,45 @@ public class ServerController(
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x =>x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(serverId, "ip link show", "|", "grep '^[0-9]'", "|", var output = await sshClient?.ExecuteCommandAsync(serverId,"ip link show" ,"|","grep '^[0-9]'","|","awk -F': ' '{print $2}'")!;
"awk -F': ' '{print $2}'")!;
if (string.IsNullOrEmpty(output)) return BadRequest(); if (string.IsNullOrEmpty(output)) return BadRequest();
var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
return Ok(data); return Ok(data);
} }
[HttpGet("GetServerNetworkEquipmentInfo")] [HttpGet("GetServerNetworkEquipmentInfo")]
public async Task<IActionResult> GetServerNetworkEquipmentInfo([FromQuery] string serverId, public async Task<IActionResult> GetServerNetworkEquipmentInfo([FromQuery] string serverId,[FromQuery] string networkId)
[FromQuery] string networkId)
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x =>x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(serverId, "/usr/sbin/ifconfig", networkId)!; var output = await sshClient?.ExecuteCommandAsync(serverId,"/usr/sbin/ifconfig",networkId)!;
if (string.IsNullOrEmpty(output)) return BadRequest(); if (string.IsNullOrEmpty(output)) return BadRequest();
output = string.Join(" ", output.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim())); output = string.Join(" ", output.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()));
var data = output.Split(" ", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).Skip(1).ToList(); var data = output.Split(" ", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).Skip(1).ToList();
return Ok(data); return Ok(data);
} }
[HttpGet("GetServerGpuList")] [HttpGet("GetServerGpuList")]
public async Task<IActionResult> GetServerGpuList([FromQuery] string serverId) public async Task<IActionResult> GetServerGpuList([FromQuery] string serverId)
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x =>x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(serverId, "lspci", "|", "awk '/VGA/ {print $1}' ")!; var output = await sshClient?.ExecuteCommandAsync(serverId,"lspci" ,"|","awk '/VGA/ {print $1}' ")!;
if (string.IsNullOrEmpty(output)) return BadRequest(); if (string.IsNullOrEmpty(output)) return BadRequest();
var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
return Ok(data); return Ok(data);
} }
[HttpGet("GetServerDiskInfo")] [HttpGet("GetServerDiskInfo")]
public async Task<IActionResult> GetServerDiskInfo([FromQuery] string serverId, [FromQuery] string diskId) public async Task<IActionResult> GetServerDiskInfo([FromQuery] string serverId,[FromQuery] string diskId)
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x =>x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var type = "ata"; var type = "ata";
if (diskId.StartsWith("nvme")) type = "nvme"; if (diskId.StartsWith("nvme")) type = "nvme";
@ -179,20 +248,20 @@ public class ServerController(
else if (diskId.StartsWith("sd")) type = "ata"; else if (diskId.StartsWith("sd")) type = "ata";
else if (diskId.StartsWith("hd")) type = "ata"; else if (diskId.StartsWith("hd")) type = "ata";
diskId = $"/dev/{diskId}"; diskId = $"/dev/{diskId}";
var output = await sshClient?.ExecuteCommandAsync(serverId, $"echo '{server.Password}'", "|", "sudo -S", var output = await sshClient?.ExecuteCommandAsync(serverId,$"echo '{server.Password}'","|","sudo -S","/usr/sbin/smartctl -i",diskId,$"-d {type}","-T permissive","2>/dev/null","|","awk 'NR>4'")!;
"/usr/sbin/smartctl -i", diskId, $"-d {type}", "-T permissive", "2>/dev/null", "|", "awk 'NR>4'")!;
if (string.IsNullOrEmpty(output)) return BadRequest(output); if (string.IsNullOrEmpty(output)) return BadRequest(output);
if (output.Contains("=== START OF INFORMATION SECTION ===")) if (output.Contains("=== START OF INFORMATION SECTION ==="))
{
//截断 //截断
output = output.Substring(output.IndexOf("=== START OF INFORMATION SECTION ===", StringComparison.Ordinal) + output = output.Substring(output.IndexOf("=== START OF INFORMATION SECTION ===", StringComparison.Ordinal) + "=== START OF INFORMATION SECTION ===".Length);
"=== START OF INFORMATION SECTION ===".Length); }
var diskInfo = output var diskInfo = output
.Split('\n', StringSplitOptions.RemoveEmptyEntries) .Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim().Split(':')) .Select(line => line.Trim().Split(':'))
.ToList().Select(x => new .ToList().Select(x => new
{ {
name = x[0].Replace(" ", "_"), name=x[0].Replace(" ","_"),
value = x[1] value=x[1]
}).ToList(); }).ToList();
return Ok(diskInfo); return Ok(diskInfo);
} }
@ -202,18 +271,14 @@ public class ServerController(
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x =>x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(serverId, var output = await sshClient?.ExecuteCommandAsync(serverId,"awk -F: '$1 != \"nobody\" &&$1 != \"build\" {print $1 \":\"$3}' /etc/passwd | sort -t: -k2nr")!;
"awk -F: '$1 != \"nobody\" &&$1 != \"build\" {print $1 \":\"$3}' /etc/passwd | sort -t: -k2nr")!;
if (string.IsNullOrEmpty(output)) return BadRequest("无法获得用户树"); if (string.IsNullOrEmpty(output)) return BadRequest("无法获得用户树");
var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
output = await sshClient?.ExecuteCommandAsync(serverId, "w -husf", "|", " awk '$2 !~ /^tty/ {print$1, $2}'", output = await sshClient?.ExecuteCommandAsync(serverId,"w -husf","|"," awk '$2 !~ /^tty/ {print$1, $2}'"," |"," sort ","|"," uniq")!;
" |", " sort ", "|", " uniq")!;
var onlineList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); var onlineList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
output = await sshClient?.ExecuteCommandAsync(serverId, output = await sshClient?.ExecuteCommandAsync(serverId,"lastlog | awk 'NR > 1 { if ($2 ~ /^**Never/) {print $1, \"-\",\"NULL\"} else {print $1,$2, substr($0,index($0,$3))}}' ")!;
"lastlog | awk 'NR > 1 { if ($2 ~ /^**Never/) {print $1, \"-\",\"NULL\"} else {print $1,$2, substr($0,index($0,$3))}}' ")
!;
if (string.IsNullOrEmpty(output)) return BadRequest("无法获得用户登录记录"); if (string.IsNullOrEmpty(output)) return BadRequest("无法获得用户登录记录");
var loginList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); var loginList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
var serverUserList = new List<ServerUserInfo>(); var serverUserList = new List<ServerUserInfo>();
@ -244,9 +309,9 @@ public class ServerController(
var len = x.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); var len = x.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
var name = len[0]; var name = len[0];
var d = serverUserList.Find(y => y.Name == name); var d = serverUserList.Find(y => y.Name == name);
if (d == null) return; if(d==null) return;
var port = len[1]; var port = len[1];
var time = string.Join(" ", len.Skip(2).ToList()); var time = string.Join(" ",len.Skip(2).ToList());
if (time != "NULL") if (time != "NULL")
{ {
DateTimeOffset.TryParseExact(time, DateTimeOffset.TryParseExact(time,
@ -256,55 +321,51 @@ public class ServerController(
out var dateTimeOffset); out var dateTimeOffset);
time = dateTimeOffset.ToString("G"); time = dateTimeOffset.ToString("G");
} }
d.LastLoginTime = time; d.LastLoginTime = time;
d.Port = port; d.Port = port;
}); });
serverUserList = serverUserList.OrderByDescending(x => x.IsOnline).ThenByDescending(x => x.LastLoginTime) serverUserList = serverUserList.OrderByDescending(x => x.IsOnline).ThenByDescending(x => x.LastLoginTime).ToList();
.ToList();
return Ok(serverUserList); return Ok(serverUserList);
} }
[HttpGet("GetServerProcessesList")] [HttpGet("GetServerProcessesList")]
public async Task<IActionResult> GetServerProcessesList([FromQuery] string serverId, [FromQuery] string? userName) public async Task<IActionResult> GetServerProcessesList([FromQuery] string serverId,[FromQuery] string? userName)
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x => x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(serverId, var output = await sshClient?.ExecuteCommandAsync(serverId,
"ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | awk 'NR>1'", userName != null ? $"| grep {userName}" : "")!; "ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | awk 'NR>1'",userName!=null?$"| grep {userName}":"" )!;
var data = output.Split("\n", StringSplitOptions.RemoveEmptyEntries); var data = output.Split("\n", StringSplitOptions.RemoveEmptyEntries);
var processList = data.Select(x => var processList = data.Select(x =>
{ {
x = x.Trim(); x=x.Trim();
var line = x.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); var line = x.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
return new return new
{ {
Pid = line[0], Pid=line[0],
User = line[1], User = line[1],
Cpu = line[2], Cpu=line[2],
Memory = line[3], Memory=line[3],
ProcessName = string.Join(" ", line.Skip(4)) ProcessName=string.Join(" ",line.Skip(4))
}; };
}).ToList(); }).ToList();
return Ok(processList); return Ok(processList);
} }
[HttpGet("GetServerProcessesKill")] [HttpGet("GetServerProcessesKill")]
public async Task<IActionResult> GetServerProcessesKill([FromQuery] string serverId, [FromQuery] string pid, public async Task<IActionResult> GetServerProcessesKill([FromQuery] string serverId, [FromQuery] string pid,[FromQuery] bool force=false)
[FromQuery] bool force = false)
{ {
var sshClient = serviceProvider.GetService<SshService>(); var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList(); var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x => x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(serverId, var output = await sshClient?.ExecuteCommandAsync(serverId,
$"echo {server.Password}", "|", "sudo -SS", "kill", force ? "-9" : "-15", pid)!; $"echo {server.Password}","|","sudo -SS","kill",force?"-9":"-15",pid )!;
return Ok($"关闭信号已发送,{output}"); return Ok($"关闭信号已发送,{output}");
} }
[HttpGet("GetServerNetworkList")] [HttpGet("GetServerNetworkList")]
public async Task<IActionResult> GetServerNetworkList([FromQuery] string serverId, [FromQuery] string? userName) public async Task<IActionResult> GetServerNetworkList([FromQuery] string serverId, [FromQuery] string? userName)
{ {
@ -313,7 +374,7 @@ public class ServerController(
var server = serverConfigs.Find(x => x.Id == serverId); var server = serverConfigs.Find(x => x.Id == serverId);
if (server == null) return BadRequest(); if (server == null) return BadRequest();
var output = await sshClient?.ExecuteCommandAsync(serverId, var output = await sshClient?.ExecuteCommandAsync(serverId,
$"echo {server.Password}", "|", "sudo -S ss -tunapo")!; $"echo {server.Password}","|","sudo -S ss -tunapo")!;
if (string.IsNullOrEmpty(output)) return BadRequest("返回为空"); if (string.IsNullOrEmpty(output)) return BadRequest("返回为空");
try try
{ {
@ -325,15 +386,15 @@ public class ServerController(
var networkList = data.Select(x => new var networkList = data.Select(x => new
{ {
netId = x[0], netId=x[0],
recvQ = x[2], recvQ=x[2],
sendQ = x[3], sendQ=x[3],
addressForm = x[4], addressForm=x[4],
addressTo = x[5], addressTo=x[5],
process = x[6].Split("),(", StringSplitOptions.RemoveEmptyEntries).Select(s => new process=x[6].Split("),(",StringSplitOptions.RemoveEmptyEntries).Select(s=>new
{ {
name = s.Split(",")[0].Replace("users:((\"", "").Replace("\"", ""), name=s.Split(",")[0].Replace("users:((\"","").Replace("\"",""),
pid = s.Split(",")[1].Split("=", StringSplitOptions.RemoveEmptyEntries)[1] pid=s.Split(",")[1].Split("=",StringSplitOptions.RemoveEmptyEntries)[1]
}) })
}); });
return Ok(networkList); return Ok(networkList);
@ -346,35 +407,35 @@ public class ServerController(
[HttpPost("UpLoadWord")] [HttpPost("UpLoadWord")]
public async Task<IActionResult> UpLoadWord( public async Task<IActionResult> UpLoadWord(
[FromQuery] string serverId, [FromQuery] string serverId,
[FromQuery] string userName, [FromQuery] string userName,
[FromQuery] string wordId, [FromQuery] string wordId,
[FromBody] WordModel content) [FromBody] WordModel content)
{ {
// 拼接路径 markdowns/ServerId/id.json // 拼接路径 markdowns/ServerId/id.json
var path = Path.Combine(AppContext.BaseDirectory, "markdowns", serverId, $"{wordId}.json"); var path = Path.Combine(AppContext.BaseDirectory,"markdowns",serverId, $"{wordId}.json");
var createAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); var createAt=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var lastModifyAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); var lastModifyAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var directoryPath = Path.GetDirectoryName(path); var directoryPath = Path.GetDirectoryName(path);
if (!Directory.Exists(directoryPath)) if (!Directory.Exists(directoryPath))
{
// 不存在则创建目录 // 不存在则创建目录
if (directoryPath != null) if (directoryPath != null) Directory.CreateDirectory(directoryPath);
Directory.CreateDirectory(directoryPath); }
// 检查文件是否存在 // 检查文件是否存在
if (System.IO.File.Exists(path)) if (System.IO.File.Exists(path))
{ {
// 读取现有JSON文件 // 读取现有JSON文件
var json = await System.IO.File.ReadAllTextAsync(path); var json = await System.IO.File.ReadAllTextAsync(path);
// 反序列化JSON为动态对象 // 反序列化JSON为动态对象
dynamic existingWord = JsonConvert.DeserializeObject(json) ?? new ExpandoObject(); dynamic existingWord = Newtonsoft.Json.JsonConvert.DeserializeObject(json) ?? new ExpandoObject();;
;
// 使用内部的createAt更新createAt // 使用内部的createAt更新createAt
createAt = existingWord.createAt; createAt = existingWord.createAt;
} }
// 创建新的配置对象 // 创建新的配置对象
var newWord = new WordFileModel var newWord = new WordFileModel
@ -385,9 +446,9 @@ public class ServerController(
WordName = content.Name, WordName = content.Name,
CreateAt = createAt, CreateAt = createAt,
LastModifyAt = lastModifyAt LastModifyAt = lastModifyAt
}; };
// 将新的配置对象序列化为JSON // 将新的配置对象序列化为JSON
var newJson = JsonConvert.SerializeObject(newWord); var newJson = Newtonsoft.Json.JsonConvert.SerializeObject(newWord);
// 覆盖写入新的JSON配置 // 覆盖写入新的JSON配置
await System.IO.File.WriteAllTextAsync(path, newJson); await System.IO.File.WriteAllTextAsync(path, newJson);
@ -400,15 +461,18 @@ public class ServerController(
public async Task<IActionResult> GetWordList([FromQuery] string serverId) public async Task<IActionResult> GetWordList([FromQuery] string serverId)
{ {
var path = Path.Combine(AppContext.BaseDirectory, "markdowns", serverId); var path = Path.Combine(AppContext.BaseDirectory, "markdowns", serverId);
if (!Directory.Exists(path)) Directory.CreateDirectory(path); if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
var files = Directory.GetFiles(path); var files = Directory.GetFiles(path);
var wordList = new List<WordFileModel>(); var wordList = new List<WordFileModel>();
foreach (var file in files) foreach (var file in files)
{ {
var fileInfo = new FileInfo(file); var fileInfo = new FileInfo(file);
var json = await System.IO.File.ReadAllTextAsync(file); var json = await System.IO.File.ReadAllTextAsync(file);
var word = JsonConvert.DeserializeObject<WordFileModel>(json); var word = Newtonsoft.Json.JsonConvert.DeserializeObject<WordFileModel>(json);
//去除content //去除content
if (word == null) continue; if (word == null) continue;
word.Content = null; word.Content = null;
@ -416,7 +480,6 @@ public class ServerController(
word.FileSize = fileSize.ToString(); word.FileSize = fileSize.ToString();
wordList.Add(word); wordList.Add(word);
} }
return Ok(wordList); return Ok(wordList);
} }
@ -425,19 +488,19 @@ public class ServerController(
{ {
var path = Path.Combine(AppContext.BaseDirectory, "markdowns", serverId, wordId + ".json"); var path = Path.Combine(AppContext.BaseDirectory, "markdowns", serverId, wordId + ".json");
var json = await System.IO.File.ReadAllTextAsync(path); var json = await System.IO.File.ReadAllTextAsync(path);
var word = JsonConvert.DeserializeObject<WordFileModel>(json); var word = Newtonsoft.Json.JsonConvert.DeserializeObject<WordFileModel>(json);
return Ok(word); return Ok(word);
} }
[HttpGet("GetWordTemplates")] [HttpGet("GetWordTemplates")]
public async Task<IActionResult> GetWordTemplates() public async Task<IActionResult> GetWordTemplates()
{ {
var path = Path.Combine(AppContext.BaseDirectory, "markdowns", "templates"); var path = Path.Combine(AppContext.BaseDirectory, "markdowns", "templates");
var files = Directory.GetFiles(path); var files = Directory.GetFiles(path);
var templates = files.Select(x => new var templates = files.Select( x => new
{ {
name = Path.GetFileName(x).Replace(".md", ""), name = Path.GetFileName(x).Replace(".md",""),
content = System.IO.File.ReadAllTextAsync(x).Result content= System.IO.File.ReadAllTextAsync(x).Result
}).ToList(); }).ToList();
return Ok(templates); return Ok(templates);
} }
@ -462,15 +525,15 @@ public class WordFileModel
public required string? Content { get; set; } public required string? Content { get; set; }
public string? FileSize { get; set; } public string? FileSize { get; set; }
public required string CreateAt { get; set; } public required string CreateAt { get; set; }
public required string WordName { get; set; } public required string WordName { get; set;}
public required string LastModifyAt { get; set; } public required string LastModifyAt { get; set; }
} }
public class ServerUserInfo public class ServerUserInfo
{ {
public required string Name { get; set; } public required string Name { get; set; }
public required bool IsOnline { get; set; } public required bool IsOnline { get; set; }
public required string? LastLoginTime { get; set; } public required string? LastLoginTime { get; set; }
public required string? Address { get; set; } public required string? Address { get; set; }
public required string? Port { get; set; } public required string? Port { get; set; }
} }

View File

@ -19,7 +19,7 @@ public class UserController(UserManager<ApplicationUser> userManager) : Controll
if (user == null) return BadRequest("用户不存在"); if (user == null) return BadRequest("用户不存在");
//获取role //获取role
var roles = await userManager.GetRolesAsync(user); var roles = await userManager.GetRolesAsync(user);
// 创建一个匿名对象,只包含您想要公开的信息 // 创建一个匿名对象,只包含您想要公开的信息
var userInfo = new var userInfo = new
{ {
@ -36,7 +36,7 @@ public class UserController(UserManager<ApplicationUser> userManager) : Controll
user.Avatar, user.Avatar,
user.Desc, user.Desc,
user.Posts, user.Posts,
Role = roles[0] Role = roles[0],
}; };
// 返回用户信息 // 返回用户信息
@ -78,7 +78,7 @@ public class UserController(UserManager<ApplicationUser> userManager) : Controll
{ {
user.Address, user.Address,
user.Avatar, user.Avatar,
CreateDate = user.CreateDate.ToLocalTime().ToString("U"), CreateDate=user.CreateDate.ToLocalTime().ToString("U"),
user.Desc, user.Desc,
user.Email, user.Email,
user.Id, user.Id,
@ -86,10 +86,10 @@ public class UserController(UserManager<ApplicationUser> userManager) : Controll
user.PhoneNumber, user.PhoneNumber,
LastLoginTime = user.LastLoginTime?.ToLocalTime().ToString("U"), LastLoginTime = user.LastLoginTime?.ToLocalTime().ToString("U"),
user.Posts, user.Posts,
ModifiedDate = user.ModifiedDate.ToLocalTime().ToString("U"), ModifiedDate=user.ModifiedDate.ToLocalTime().ToString("U"),
user.UserName, user.UserName,
user.PhysicalAddress, user.PhysicalAddress,
IsLock = user.LockoutEnd > DateTimeOffset.UtcNow IsLock=user.LockoutEnd > DateTimeOffset.UtcNow
}).ToList(); }).ToList();
return Ok(userInfoList); return Ok(userInfoList);
} }

View File

@ -1,45 +1,63 @@
namespace LoongPanel_Asp.Helpers; using LoongPanel_Asp.Hubs;
using Microsoft.AspNetCore.SignalR;
public static class DataHelper namespace LoongPanel_Asp.Helpers;
public class DataHelper(ApplicationDbContext dbContext,IHubContext<SessionHub> context)
{ {
public static void AddMonitoringData(List<ServerMonitoringData> res, string data, string dataName, string dataType) public async Task SaveData(ServerMonitoringData data)
{ {
res.Add(new ServerMonitoringData // 保存数据到数据库
{ var dataDb = dbContext.ServerMonitoringData;
ServerId = null, //获取当前时间
Data = data, var time = DateTime.UtcNow;
DataName = dataName, data.Time = time;
DataType = dataType dataDb.Add(data);
}); //提交
await dbContext.SaveChangesAsync();
}
//批量添加
public async Task SaveData(List<ServerMonitoringData> data)
{
var dataDb = dbContext.ServerMonitoringData;
var time = DateTime.UtcNow;
foreach (var i in data)
{
i.Time = time;
dataDb.Add(i);
}
await dbContext.SaveChangesAsync();
}
public async Task CheckData(string serverId,string valueType,string value,string valueName)
{
var alertConfigs = JobConfigHelper.GetAlerts();
if (!alertConfigs.TryGetValue(serverId, out var serverAlert)) return;
serverAlert.Notify.TryGetValue(valueType, out var notifyValuePairs);
serverAlert.Warning.TryGetValue(valueType, out var warningValuePairs);
var matchingValues = warningValuePairs?.Where(pair => double.Parse(value) >= double.Parse(pair.Key))
.SelectMany(pair => pair.Value).Distinct().ToList();
if (matchingValues?.Count > 0)
{
foreach (var item in matchingValues)
{
await context.Clients.Group(item).SendAsync("ReceiveWaring", value, valueName);
var key=$"{serverId}_{valueType}+{item}";
}
return;
}
matchingValues = notifyValuePairs?.Where(pair => double.Parse(value) >= double.Parse(pair.Key))
.SelectMany(pair => pair.Value).Distinct().ToList();
if (matchingValues?.Count > 0)
{
foreach (var item in matchingValues)
{
await context.Clients.Group(item).SendAsync("ReceiveNotify", value, valueName);
}
}
} }
// public async Task CheckData(string serverId,string valueType,string value,string valueName)
// {
//
// var alertConfigs = JobConfigHelper.GetAlerts();
// if (!alertConfigs.TryGetValue(serverId, out var serverAlert)) return;
// serverAlert.Notify.TryGetValue(valueType, out var notifyValuePairs);
// serverAlert.Warning.TryGetValue(valueType, out var warningValuePairs);
// var matchingValues = warningValuePairs?.Where(pair => double.Parse(value) >= double.Parse(pair.Key))
// .SelectMany(pair => pair.Value).Distinct().ToList();
//
// if (matchingValues?.Count > 0)
// {
// foreach (var item in matchingValues)
// {
// await context.Clients.Group(item).SendAsync("ReceiveWaring", value, valueName);
// var key=$"{serverId}_{valueType}+{item}";
// }
// return;
// }
//
// matchingValues = notifyValuePairs?.Where(pair => double.Parse(value) >= double.Parse(pair.Key))
// .SelectMany(pair => pair.Value).Distinct().ToList();
// if (matchingValues?.Count > 0)
// {
// foreach (var item in matchingValues)
// {
// await context.Clients.Group(item).SendAsync("ReceiveNotify", value, valueName);
// }
// }
// }
} }

View File

@ -33,7 +33,7 @@ public static class JobConfigHelper
Description = section.Keys["Description"], Description = section.Keys["Description"],
CronExpression = section.Keys["CronExpression"], CronExpression = section.Keys["CronExpression"],
ValueName = section.Keys["ValueName"], ValueName = section.Keys["ValueName"],
Executor = section.Keys["Executor"]?.Split(',').Select(executor => Executor = section.Keys["Executor"].Split(',').Select(executor =>
{ {
// 查找serverConfigs 中id匹配 未找到则跳过 // 查找serverConfigs 中id匹配 未找到则跳过
var server = serverConfigs.FirstOrDefault(server => server.Id == executor); var server = serverConfigs.FirstOrDefault(server => server.Id == executor);
@ -71,7 +71,7 @@ public static class JobConfigHelper
return _serverConfigs; return _serverConfigs;
} }
public static Dictionary<string, AlertConfiguration> GetAlerts() public static Dictionary<string,AlertConfiguration> GetAlerts()
{ {
if (_alertsConfigs != null) return _alertsConfigs; if (_alertsConfigs != null) return _alertsConfigs;
@ -81,11 +81,11 @@ public static class JobConfigHelper
var alertsFolderPath = Path.Combine(Environment.CurrentDirectory, "Configs", "Alerts"); var alertsFolderPath = Path.Combine(Environment.CurrentDirectory, "Configs", "Alerts");
// 检查目录是否存在,如果不存在则创建 // 检查目录是否存在,如果不存在则创建
if (!Directory.Exists(alertsFolderPath)) Directory.CreateDirectory(alertsFolderPath); if (!Directory.Exists(alertsFolderPath)) Directory.CreateDirectory(alertsFolderPath);
// 获取目录下所有.ini文件的路径 // 获取目录下所有.ini文件的路径
var alertFiles = Directory.GetFiles(alertsFolderPath, "*.ini"); var alertFiles = Directory.GetFiles(alertsFolderPath, "*.ini");
var notifyUsersMap = new Dictionary<string, Dictionary<string, Dictionary<string, List<string>>>>(); var notifyUsersMap = new Dictionary<string,Dictionary<string, Dictionary<string, List<string>>>>();
var warningUsersMap = new Dictionary<string, Dictionary<string, Dictionary<string, List<string>>>>(); var warningUsersMap = new Dictionary<string,Dictionary<string, Dictionary<string, List<string>>>>();
// 读取配置信息 // 读取配置信息
foreach (var alertFile in alertFiles) foreach (var alertFile in alertFiles)
{ {
@ -97,7 +97,7 @@ public static class JobConfigHelper
{ {
var serverId = alertSection.SectionName.Split("_")[1]; var serverId = alertSection.SectionName.Split("_")[1];
var type = alertSection.SectionName.Split("_")[0]; var type = alertSection.SectionName.Split("_")[0];
var notifyValue = alertSection.Keys["Notify"]; var notifyValue= alertSection.Keys["Notify"];
var warningValue = alertSection.Keys["Warning"]; var warningValue = alertSection.Keys["Warning"];
var userId = Path.GetFileNameWithoutExtension(alertFile); var userId = Path.GetFileNameWithoutExtension(alertFile);
if (!notifyUsersMap.TryGetValue(serverId, out var notifyTypes)) if (!notifyUsersMap.TryGetValue(serverId, out var notifyTypes))
@ -111,7 +111,6 @@ public static class JobConfigHelper
warningTypes = []; warningTypes = [];
warningUsersMap.Add(serverId, warningTypes); warningUsersMap.Add(serverId, warningTypes);
} }
if (!notifyTypes.TryGetValue(type, out var notifyValues)) if (!notifyTypes.TryGetValue(type, out var notifyValues))
{ {
notifyValues = []; notifyValues = [];
@ -129,30 +128,25 @@ public static class JobConfigHelper
notifyUsersList = []; notifyUsersList = [];
notifyValues.Add(notifyValue, notifyUsersList); notifyValues.Add(notifyValue, notifyUsersList);
} }
if (!warningValues.TryGetValue(warningValue, out var warningUsersList)) if (!warningValues.TryGetValue(warningValue, out var warningUsersList))
{ {
warningUsersList = []; warningUsersList = [];
warningValues.Add(warningValue, warningUsersList); warningValues.Add(warningValue, warningUsersList);
} }
notifyUsersList.Add(userId); notifyUsersList.Add(userId);
warningUsersList.Add(userId); warningUsersList.Add(userId);
} }
} }
//遍历 notifyUsersMap //遍历 notifyUsersMap
foreach (var (serverId, notifyUsersList) in notifyUsersMap) foreach (var (serverId, notifyUsersList) in notifyUsersMap)
{ {
//获得key 和 value //获得key 和 value
if (!alertsConfigs.TryGetValue(serverId, out var alertsConfig)) if(!alertsConfigs.TryGetValue(serverId, out var alertsConfig)){
{
//创建新的 //创建新的
alertsConfig = new AlertConfiguration(); alertsConfig = new AlertConfiguration();
alertsConfigs.Add(serverId, alertsConfig); alertsConfigs.Add(serverId, alertsConfig);
} }
alertsConfig.Notify= notifyUsersList;
alertsConfig.Notify = notifyUsersList;
} }
foreach (var (serverId, emailUsersList) in warningUsersMap) foreach (var (serverId, emailUsersList) in warningUsersMap)
@ -164,20 +158,19 @@ public static class JobConfigHelper
alertsConfig = new AlertConfiguration(); alertsConfig = new AlertConfiguration();
alertsConfigs.Add(serverId, alertsConfig); alertsConfigs.Add(serverId, alertsConfig);
} }
alertsConfig.Warning = emailUsersList; alertsConfig.Warning = emailUsersList;
} }
_alertsConfigs = alertsConfigs; _alertsConfigs =alertsConfigs;
return alertsConfigs; return alertsConfigs;
} }
public static void ReloadAlerts() public static void ReloadAlerts()
{ {
_alertsConfigs = null; _alertsConfigs = null;
GetAlerts(); GetAlerts();
} }
public static void ReloadServers() public static void ReloadServers()
{ {
_serverConfigs = null; _serverConfigs = null;
@ -197,7 +190,7 @@ public class JobConfiguration
public string? Description { get; init; } public string? Description { get; init; }
//执行者 //执行者
public List<ServerModel?>? Executor { get; init; } public required List<ServerModel?> Executor { get; init; }
//任务类型 //任务类型
public required string JobType { get; init; } public required string JobType { get; init; }
@ -209,10 +202,9 @@ public class JobConfiguration
public string? ValueName { get; init; } public string? ValueName { get; init; }
} }
public class AlertConfiguration( public class AlertConfiguration(Dictionary<string, Dictionary<string, List<string>>>? notify=null,
Dictionary<string, Dictionary<string, List<string>>>? notify = null, Dictionary<string, Dictionary<string, List<string>>>? warning=null)
Dictionary<string, Dictionary<string, List<string>>>? warning = null)
{ {
public Dictionary<string, Dictionary<string, List<string>>> Notify { get; set; } = notify ?? []; public Dictionary<string, Dictionary<string, List<string>>> Notify { get; set; } = notify ?? [];
public Dictionary<string, Dictionary<string, List<string>>> Warning { get; set; } = warning ?? []; public Dictionary<string, Dictionary<string, List<string>>> Warning { get; set; } = warning ?? [];
} }

View File

@ -17,7 +17,7 @@ public class SessionHub(UserManager<ApplicationUser> userManager, ILiteDatabase
// 查询获取用户对象 // 查询获取用户对象
var user = await userManager.FindByIdAsync(userId); var user = await userManager.FindByIdAsync(userId);
user!.Address = ip?.ToString(); user!.Address = ip?.ToString();
user!.LastLoginTime = DateTime.UtcNow; user!.LastLoginTime = DateTime.UtcNow;
// 更新用户信息 // 更新用户信息
await userManager.UpdateAsync(user); await userManager.UpdateAsync(user);
@ -31,7 +31,7 @@ public class SessionHub(UserManager<ApplicationUser> userManager, ILiteDatabase
NickName = user.NickName!, NickName = user.NickName!,
Avatar = user.Avatar, Avatar = user.Avatar,
Role = role.ToList(), Role = role.ToList(),
Posts = user.Posts Posts = user.Posts,
}; };
@ -64,11 +64,12 @@ public class SessionHub(UserManager<ApplicationUser> userManager, ILiteDatabase
await base.OnDisconnectedAsync(exception); await base.OnDisconnectedAsync(exception);
} }
public async Task SendMessage(string userId, string receiver, string message) public async Task SendMessage(string userId, string receiver ,string message)
{ {
Console.WriteLine("12312312312{0},{1}", userId, message); Console.WriteLine("12312312312{0},{1}", userId, message);
await Clients.Group(receiver).SendAsync("sendMessage", userId, message); await Clients.Group(receiver).SendAsync("sendMessage", userId, message);
} }
} }
//定义类型UserInfo //定义类型UserInfo
@ -81,6 +82,6 @@ public class UserInfo
public required string Avatar { get; init; } public required string Avatar { get; init; }
public required List<string> Role { get; init; } public required List<string> Role { get; init; }
public required string Posts { get; init; } public required string Posts { get; init;}
} }

View File

@ -4,35 +4,34 @@ using Microsoft.AspNetCore.SignalR;
namespace LoongPanel_Asp.Hubs; namespace LoongPanel_Asp.Hubs;
public class TerminalHub(SshStreamService sshStreamService) : Hub public class TerminalHub(SshStreamService sshStreamService):Hub
{ {
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
{ {
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
await Groups.AddToGroupAsync(Context.ConnectionId, userId); await Groups.AddToGroupAsync(Context.ConnectionId, userId);
} }
//create a terminal //create a terminal
public void CreateTerminal(string serverId) public void CreateTerminal(string serverId)
{ {
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
sshStreamService.Connect(userId, serverId); sshStreamService.Connect(userId, serverId);
} }
//send a message to the terminal //send a message to the terminal
public void SendMessage(string message) public void SendMessage(string message)
{ {
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
Console.WriteLine(message); Console.WriteLine(message);
sshStreamService.Write(userId, message); sshStreamService.Write(userId,message);
} }
public void ResizeTerminal(int row, int col) public void ResizeTerminal(int row, int col)
{ {
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
sshStreamService.ReSize(userId, col, row); sshStreamService.ReSize(userId,col,row);
} }
//断开 //断开
public override Task OnDisconnectedAsync(Exception? exception) public override Task OnDisconnectedAsync(Exception? exception)
{ {

View File

@ -1,52 +1,62 @@
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
using LoongPanel_Asp.Helpers;
namespace LoongPanel_Asp; namespace LoongPanel_Asp
public class Init : IHostedService
{ {
private readonly IServiceProvider _serviceProvider; public class Init : IHostedService
public Init(IServiceProvider serviceProvider)
{ {
_serviceProvider = serviceProvider; private readonly IServiceProvider _serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken) public Init(IServiceProvider serviceProvider)
{
using var scope = _serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
// 检查管理员用户是否存在
var adminUser = await userManager.FindByNameAsync("admin");
if (adminUser == null)
{ {
adminUser = new ApplicationUser _serviceProvider = serviceProvider;
{ }
Avatar = "https://api.multiavatar.com/admin.svg",
Posts = "管理员",
CreateDate = DateTime.UtcNow,
ModifiedDate = DateTime.UtcNow,
Email = "admin@admin.com",
UserName = "admin",
PhoneNumber = "999999999",
NickName = "默认管理员"
};
var result = await userManager.CreateAsync(adminUser, "Qwertyuiop123!@#"); public async Task StartAsync(CancellationToken cancellationToken)
//分配管理员角色 {
if (result.Succeeded) using var scope = _serviceProvider.CreateScope();
await userManager.AddToRoleAsync(adminUser, "admin"); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
if (result.Succeeded) var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
Console.WriteLine("管理员创建成功,账号:{0},密码:{1}", "admin", "Qwertyuiop123!@#"); // 检查管理员用户是否存在
else var adminUser = await userManager.FindByNameAsync("admin");
foreach (var error in result.Errors) if (adminUser == null)
Console.WriteLine(error.Description); {
adminUser = new ApplicationUser
{
Avatar = "https://api.multiavatar.com/admin.svg",
Posts = "管理员",
CreateDate = DateTime.UtcNow,
ModifiedDate = DateTime.UtcNow,
Email = "admin@admin.com",
UserName = "admin",
PhoneNumber = "999999999",
NickName = "默认管理员",
};
var result = await userManager.CreateAsync(adminUser, "Qwertyuiop123!@#");
//分配管理员角色
if (result.Succeeded)
await userManager.AddToRoleAsync(adminUser, "admin");
if (result.Succeeded)
Console.WriteLine("管理员创建成功,账号:{0},密码:{1}", "admin", "Qwertyuiop123!@#");
else
foreach (var error in result.Errors)
{
Console.WriteLine(error.Description);
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
// 这里可以执行一些应用程序停止时的清理操作
return Task.CompletedTask;
} }
} }
}
public Task StopAsync(CancellationToken cancellationToken)
{
// 这里可以执行一些应用程序停止时的清理操作
return Task.CompletedTask;
}
}

View File

@ -1,53 +1,183 @@
using System.Globalization; using System.Globalization;
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Models; using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers; using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.SignalR;
using Quartz; using Quartz;
using Property = LoongPanel_Asp.Models.Property;
namespace LoongPanel_Asp.Jobs; namespace LoongPanel_Asp.Jobs;
public class CpuTotalJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class CpuTotalJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
public async Task Execute(IJobExecutionContext context)
{ {
var output = await sshService.ExecuteCommandAsync(server.Id, "sar", "-u", "3 1", "|", "grep", "Average"); var dataMap = context.JobDetail.JobDataMap;
if (string.IsNullOrEmpty(output)) return; var dataHelper = new DataHelper(dbContext,hubContext);
var values = output.Split(' ', StringSplitOptions.RemoveEmptyEntries).Skip(2).ToList(); var serverList = (List<ServerModel>)dataMap["executor"];
DataHelper.AddMonitoringData(res, values[0], "CPU用户使用率", "CpuUserUsage"); var cpuDataListAll = new List<ServerMonitoringData>();
DataHelper.AddMonitoringData(res, values[2], "CPU系统使用率", "CpuSystemUsage"); var sshClient = serviceProvider.GetService<SshService>();
DataHelper.AddMonitoringData(res, values[3], "CPUIO等待率", "CpuIOWaitUsage"); foreach (var server in serverList)
DataHelper.AddMonitoringData(res, (100 - double.Parse(values[5])).ToString(CultureInfo.InvariantCulture), {
"CPU总使用率", "CpuTotalUsage"); var output = await sshClient?.ExecuteCommandAsync(server.Id, "sar", "-u", "3 1", "|", "grep", "Average")!;
if (string.IsNullOrEmpty(output)) continue;
output = output.Replace("Average:", "").Replace("all", "").TrimStart();
var cpuList = output.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var cpuProperties = new List<Property>
{
new() { Name = "CpuUserUsage", DisplayName = "CPU用户使用率", Order = 0 },
new() { Name = "CpuSystemUsage", DisplayName = "CPU系统使用率", Order = 1 },
new() { Name = "CpuIOWaitUsage", DisplayName = "CPUIO等待使用率", Order = 2 }
};
var cpuDataList = cpuProperties.Select(property => new ServerMonitoringData { ServerId = server.Id, Data = cpuList[(int)property.Order!], DataName = property.DisplayName, DataType = property.Name }).ToList();
// Calculate CpuTotalUsage separately
var idleUsage = double.Parse(cpuList[5]);
var totalUsage = new ServerMonitoringData
{
ServerId = server.Id,
Data = (100 - idleUsage).ToString(CultureInfo.InvariantCulture),
DataName = "CPU总使用率",
DataType = "CpuTotalUsage"
};
cpuDataList.Add(totalUsage);
cpuDataList.ForEach(async data =>
{
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
await dataHelper.CheckData(server.Id, data.DataType??"", data.Data??"", data.DataName);
});
cpuDataListAll.AddRange(cpuDataList);
}
_count++;
if (_count <= 10) return;
_count = 0;
// Add to database
await dataHelper.SaveData(cpuDataListAll);
} }
} }
public class CpuSpeedJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class CpuSpeedJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
public async Task Execute(IJobExecutionContext context)
{ {
var output = await sshService.ExecuteCommandAsync(server.Id, "cat", "/proc/cpuinfo", "|", "grep", "'cpu MHz'"); var dataMap = context.JobDetail.JobDataMap;
if (string.IsNullOrEmpty(output)) return; var dataHelper = new DataHelper(dbContext,hubContext);
var cpuSpeeds = output.Split('\n', StringSplitOptions.RemoveEmptyEntries) var serverList = (List<ServerModel>)dataMap["executor"];
.Select(line => line.Split(':', 2)[1].Trim()) var cpuDataListAll = new List<ServerMonitoringData>();
.Where(speed => !string.IsNullOrEmpty(speed)).ToList(); var sshClient = serviceProvider.GetService<SshService>();
DataHelper.AddMonitoringData(res, cpuSpeeds[0], "CPU总频率", "CpuTotalSpeed"); foreach (var server in serverList)
cpuSpeeds.Skip(1).Select((speed, i) => (speed, i)).ToList().ForEach(x => {
DataHelper.AddMonitoringData(res, x.speed, $"CPU{x.i + 1}频率", $"CpuSingleSpeed-{x.i + 1}")); var output =
await sshClient?.ExecuteCommandAsync(server.Id, "cat", "/proc/cpuinfo", "|", "grep", "'cpu MHz'")!;
if (string.IsNullOrEmpty(output)) continue;
//切分每行
var cpuSpeedList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
//获取第一行
var cpuTotalSpeed = cpuSpeedList[0].Split(':', StringSplitOptions.RemoveEmptyEntries)[1].Trim();
var cpuDataList = new List<ServerMonitoringData>();
var totalSpeed = new ServerMonitoringData
{
ServerId = server.Id,
Data = cpuTotalSpeed,
DataName = "CPU总速度",
DataType = "CpuTotalSpeed"
};
cpuDataList.Add(totalSpeed);
//遍历剩下的行
foreach (var (cpuSpeed, index) in cpuSpeedList.Skip(1).Select((x, index) => (x, index)))
{
var speed = cpuSpeed.Split(':', StringSplitOptions.RemoveEmptyEntries)[1].Trim();
var singleSpeed = new ServerMonitoringData
{
ServerId = server.Id,
Data = speed,
DataName = $"CPU单核速度-{index}",
DataType = $"CpuSingleSpeed-{index}"
};
cpuDataList.Add(singleSpeed);
}
cpuDataListAll.AddRange(cpuDataList);
cpuDataList.ForEach(Action);
continue;
async void Action(ServerMonitoringData data)
{
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
}
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(cpuDataListAll);
} }
} }
public class CpuSingleUsageJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class CpuSingleUsageJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
public async Task Execute(IJobExecutionContext context)
{ {
var output = await sshService.ExecuteCommandAsync(server.Id, "sar -P ALL 3 1", "|", "grep Average", "|", var dataMap = context.JobDetail.JobDataMap;
"awk 'NR>2 {print 100-$NF}'"); var dataHelper = new DataHelper(dbContext,hubContext);
if (string.IsNullOrEmpty(output)) return; var serverList = (List<ServerModel>)dataMap["executor"];
var values = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); var cpuDataListAll = new List<ServerMonitoringData>();
values.Select((value, i) => (value, i)).ToList().ForEach(x => foreach (var server in serverList)
DataHelper.AddMonitoringData(res, x.value, $"CPU{x.i}使用率", $"CpuSingleUsage-{x.i}")); {
var sshClient = serviceProvider.GetService<SshService>();
var output =
await sshClient?.ExecuteCommandAsync(server.Id, "sar -P ALL 3 1", "|", "grep Average", "|",
"awk 'NR>2 {print 100-$NF}'")!;
if (string.IsNullOrEmpty(output)) continue;
var cpuDataList = new List<ServerMonitoringData>();
foreach (var (cpuUsage, index) in output.Split("\n", StringSplitOptions.RemoveEmptyEntries)
.Select((x, index) => (x, index)))
{
var singleUsage = new ServerMonitoringData
{
ServerId = server.Id,
Data = cpuUsage,
DataName = $"CPU单核使用率-{index}",
DataType = $"CpuSingleUsage-{index}"
};
cpuDataList.Add(singleUsage);
}
cpuDataList.ForEach(Action);
continue;
async void Action(ServerMonitoringData data)
{
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
}
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(cpuDataListAll);
} }
} }

View File

@ -1,39 +1,137 @@
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Models; using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers; using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.SignalR;
using Quartz; using Quartz;
namespace LoongPanel_Asp.Jobs; namespace LoongPanel_Asp.Jobs;
public class DiskTotalJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class DiskTotalJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
public async Task Execute(IJobExecutionContext context)
{ {
var output = await sshService.ExecuteCommandAsync(server.Id, "df", "--total", "|", "grep", "total"); var dataMap = context.JobDetail.JobDataMap;
if (string.IsNullOrEmpty(output)) return; var dataHelper = new DataHelper(dbContext,hubContext);
var diskTotal = output.Split(" ", StringSplitOptions.RemoveEmptyEntries)[4].Replace("%", ""); var serverList = (List<ServerModel>)dataMap["executor"];
DataHelper.AddMonitoringData(res, diskTotal, "磁盘总使用率", "DiskTotalUsage"); //获得cpu信息
var diskDataListAll = new List<ServerMonitoringData>();
foreach (var server in serverList)
{
var sshClient = serviceProvider.GetService<SshService>();
var diskDataList = new List<ServerMonitoringData>();
var output = await sshClient?.ExecuteCommandAsync(server.Id, "df", "--total", "|", "grep", "total")!;
if (string.IsNullOrEmpty(output)) return;
var disk = output.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var diskTotalUsage = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[4].Replace("%", ""),
DataName = "磁盘总使用率",
DataType = "DiskTotalUsage"
};
await hubContext.Clients.All.SendAsync("ReceiveData", diskTotalUsage.ServerId, diskTotalUsage.DataType,
diskTotalUsage.Data);
await dataHelper.CheckData(server.Id, diskTotalUsage.DataType ?? "", diskTotalUsage.Data ?? "", "磁盘总使用率");
diskDataList.Add(diskTotalUsage);
diskDataListAll.AddRange(diskDataList);
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(diskDataListAll);
} }
} }
public class DiskUseJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class DiskUseJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
{
var output = await sshService.ExecuteCommandAsync(server.Id,
"sar -d 3 1 | grep Average: | awk 'NR>1' | awk '{$1=\"\";print$0}'");
if (string.IsNullOrEmpty(output)) return;
output.Split("\n").Select(x => x.Trim()).Where(line => !string.IsNullOrEmpty(line)).ToList().ForEach(line => public async Task Execute(IJobExecutionContext context)
{
var dataMap = context.JobDetail.JobDataMap;
var dataHelper = new DataHelper(dbContext,hubContext);
var serverList = (List<ServerModel>)dataMap["executor"];
//获得cpu信息
var diskDataListAll = new List<ServerMonitoringData>();
foreach (var server in serverList)
{ {
var diskValues = line.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); var sshClient = serviceProvider.GetService<SshService>();
var diskName = diskValues[0]; var diskDataList = new List<ServerMonitoringData>();
DataHelper.AddMonitoringData(res, diskValues[1], $"磁盘每秒传输数-{diskName}", $"DiskTps-{diskName}"); var output =
DataHelper.AddMonitoringData(res, diskValues[2], $"磁盘每秒读数-{diskName}", $"DiskReadKB-{diskName}"); await sshClient?.ExecuteCommandAsync(server.Id, "sar -d 3 1", "|", "grep Average:", "|", "awk 'NR>1'","|","awk '{$1=\"\";print $0}'")!;
DataHelper.AddMonitoringData(res, diskValues[3], $"磁盘每秒写数-{diskName}", $"DiskWriteKB-{diskName}"); if (string.IsNullOrEmpty(output)) return;
DataHelper.AddMonitoringData(res, diskValues[8], $"磁盘利用率-{diskName}", $"DiskUtil-{diskName}"); var lines = output.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
});
foreach (var line in lines)
{
var disk = line.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var dev = disk[0];
// 每秒传输数
var diskTps = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[1],
DataName = $"磁盘每秒传输数-{dev}" ,
DataType = $"DiskTps-{dev}"
};
diskDataList.Add(diskTps);
var diskReadKb = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[2],
DataName = $"磁盘每秒读取数据量-{dev}" ,
DataType = $"DiskReadKB-{dev}"
};
diskDataList.Add(diskReadKb);
var diskWriteKb = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[3],
DataName = $"磁盘每秒写入数据量-{dev}" ,
DataType = $"DiskWriteKB-{dev}"
};
diskDataList.Add(diskWriteKb);
var diskAwait = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[7],
DataName = $"磁盘平均等待时间-{dev}" ,
DataType = $"DiskAwait-{dev}"
};
diskDataList.Add(diskAwait);
var diskUtil = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[8],
DataName = $"磁盘利用率-{dev}" ,
DataType = $"DiskUtil-{dev}"
};
diskDataList.Add(diskUtil);
diskDataList.ForEach(Action);
continue;
async void Action(ServerMonitoringData data)
{
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
}
}
diskDataListAll.AddRange(diskDataList);
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(diskDataListAll);
} }
} }

View File

@ -1,40 +0,0 @@
using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers;
using Quartz;
namespace LoongPanel_Asp.Jobs;
public abstract class IiJob(
DataService dataService,
SshService sshService)
: IJob
{
public async Task Execute(IJobExecutionContext context)
{
var dataMap = context.JobDetail.JobDataMap;
var serverList = (List<ServerModel>)dataMap["executor"];
var res = new List<ServerMonitoringData>();
foreach (var server in serverList)
{
var r = new List<ServerMonitoringData>();
await ExecuteInternal(context, server, sshService, r);
r.ForEach(x => x.ServerId = server.Id);
res.AddRange(r);
}
await dataService.Save(res);
}
protected abstract Task ExecuteInternal(IJobExecutionContext context, ServerModel server, SshService sshService,
List<ServerMonitoringData> res);
}
//DataService Submit
public class DataJob(DataService service) : IJob
{
public Task Execute(IJobExecutionContext context)
{
_ = service.Submit();
return Task.CompletedTask;
}
}

View File

@ -1,35 +1,99 @@
using System.Globalization; using System.Globalization;
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Models; using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers; using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.SignalR;
using Quartz; using Quartz;
namespace LoongPanel_Asp.Jobs; namespace LoongPanel_Asp.Jobs;
public class MemoryTotalJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class MemoryTotalJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
{
var output =
await sshService.ExecuteCommandAsync(server.Id, "free -w | awk 'NR>1' | awk '{$1=\"\";print$0}' | xargs");
var values = output.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var memoryTotal = values[0];
var memoryUsed = values[1];
var memoryFree = values[2];
var swapTotal = values[6];
var swapUsed = values[7];
DataHelper.AddMonitoringData(res, memoryUsed, "内存使用量", "MemoryUsed");
DataHelper.AddMonitoringData(res, memoryFree, "内存空闲量", "MemoryFree");
var memoryUsedPercent = Math.Round(double.Parse(memoryUsed) / double.Parse(memoryTotal) * 100, 2)
.ToString(CultureInfo.InvariantCulture);
DataHelper.AddMonitoringData(res, memoryUsedPercent, "内存总使用率", "MemoryTotalUsage");
if (double.Parse(swapTotal) > 0) public async Task Execute(IJobExecutionContext context)
{
var dataMap = context.JobDetail.JobDataMap;
var dataHelper = new DataHelper(dbContext,hubContext);
// 从JobDataMap中获取参数
var serverList = (List<ServerModel>)dataMap["executor"];
var serverDataListAll = new List<ServerMonitoringData>();
foreach (var server in serverList)
{ {
var swapUsedPercent = Math.Round(double.Parse(swapUsed) / double.Parse(swapTotal) * 100, 2) var sshClient = serviceProvider.GetService<SshService>();
.ToString(CultureInfo.InvariantCulture); var output = await sshClient?.ExecuteCommandAsync(server.Id, "free -w", "|", "awk 'NR>1'", "|",
DataHelper.AddMonitoringData(res, swapUsedPercent, "Swap总使用率", "SwapTotalUsage"); "awk '{$1=\"\";print $0}'", "|", "xargs")!;
if (string.IsNullOrEmpty(output)) continue;
var serverDataList = new List<ServerMonitoringData>();
var dataList = output.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var memoryProperties = new List<Property>
{
new() { Name = "MemoryTotal", DisplayName = "内存总量", Order = 0 },
new() { Name = "MemoryUsed", DisplayName = "内存使用量", Order = 1 },
new() { Name = "MemoryFree", DisplayName = "内存空闲量", Order = 2 },
new() { Name = "MemoryCache", DisplayName = "内存缓存量", Order = 5 },
new() { Name = "SwapTotal", DisplayName = "Swap总量", Order = 7 },
new() { Name = "SwapUsed", DisplayName = "Swap使用量", Order = 8 },
new() { Name = "SwapFree", DisplayName = "Swap空闲量", Order = 9 }
};
memoryProperties.ForEach(data =>
{
var d = new ServerMonitoringData
{
ServerId = server.Id,
Data = double.Parse(dataList[(int)data.Order!]).ToString(CultureInfo.CurrentCulture),
DataName = data.DisplayName,
DataType = data.Name
};
serverDataList.Add(d);
});
//计算内存使用率
var memoryUsed = double.Parse(dataList[1]);
var memoryTotal = double.Parse(dataList[0]);
if (memoryTotal <= 0) memoryTotal = 1;
var memoryUsedRate = memoryUsed / memoryTotal * 100;
var memoryData = new ServerMonitoringData
{
ServerId = server.Id,
Data = memoryUsedRate.ToString(CultureInfo.InvariantCulture),
DataName = "内存总使用率",
DataType = "MemoryTotalUsage"
};
serverDataList.Add(memoryData);
//计算交换使用率
var swapUsed = double.Parse(dataList[8]);
var swapTotal = double.Parse(dataList[7]);
if (swapTotal <= 0) swapTotal = 1;
var swapUsedRate = swapUsed / swapTotal * 100;
var swapData = new ServerMonitoringData
{
ServerId = server.Id,
Data = swapUsedRate.ToString(CultureInfo.InvariantCulture),
DataName = "Swap使用率",
DataType = "SwapTotalUsage"
};
serverDataList.Add(swapData);
serverDataList.ForEach(Action);
serverDataListAll.AddRange(serverDataList);
continue;
async void Action(ServerMonitoringData data)
{
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
}
} }
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(serverDataListAll);
} }
} }

View File

@ -1,33 +1,84 @@
using System.Globalization; using System.Globalization;
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Models; using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers; using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.SignalR;
using Quartz; using Quartz;
namespace LoongPanel_Asp.Jobs; namespace LoongPanel_Asp.Jobs;
public class NetworkTotalJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class NetworkTotalJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
public async Task Execute(IJobExecutionContext context)
{ {
var output = await sshService.ExecuteCommandAsync(server.Id, "sar", "-n", "DEV", "3 1", "|", "grep", "Average:", var dataMap = context.JobDetail.JobDataMap;
"|", "awk 'NR>1'", "|", "awk '{$1=\"\";print $0}'"); var dataHelper = new DataHelper(dbContext,hubContext);
if (string.IsNullOrEmpty(output)) return; var serverList = (List<ServerModel>)dataMap["executor"];
var lines = output.Split("\n", StringSplitOptions.RemoveEmptyEntries); var netWorkDataListAll = new List<ServerMonitoringData>();
var totalUsage = 0.0; foreach (var server in serverList)
foreach (var line in lines)
{ {
if (string.IsNullOrEmpty(line)) continue; var sshClient = serviceProvider.GetService<SshService>();
var values = line.Split(" ", StringSplitOptions.RemoveEmptyEntries); var netWorkDataList = new List<ServerMonitoringData>();
var iFace = values[0]; var output = await sshClient?.ExecuteCommandAsync(server.Id, "sar", "-n", "DEV", "3 1", "|", "grep",
DataHelper.AddMonitoringData(res, values[1], $"每秒钟接收到的数据包数量-{iFace}", $"NetWorkReceive-{iFace}"); "Average:", "|", "awk 'NR>1'", "|", "awk '{$1=\"\";print $0}'")!;
DataHelper.AddMonitoringData(res, values[2], $"每秒钟发送的数据包数量-{iFace}", $"NetWorkSend-{iFace}"); if (string.IsNullOrEmpty(output)) continue;
DataHelper.AddMonitoringData(res, values[8], $"网络接口的使用率-{iFace}", $"NetWorkUsage-{iFace}"); var lines = output.Split("\n");
totalUsage += double.Parse(values[8]); var totalUsage = 0.0;
foreach (var line in lines)
{
if (string.IsNullOrEmpty(line)) continue;
var data = line.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var iFace = data[0];
var dataNum = data.Skip(1).Select(double.Parse).ToList();
var netWorkProperties = new List<Property>
{
new() { Name = "ReceivedPacketsPerSecond", DisplayName = "每秒钟接收到的数据包数量", Order = 0 },
new() { Name = "TransmittedPacketsPerSecond", DisplayName = "每秒钟发送的数据包数量", Order = 1 },
new() { Name = "InterfaceUtilizationPercentage", DisplayName = "网络接口的使用率", Order = 7 }
};
netWorkProperties.ForEach(property =>
{
var d = new ServerMonitoringData
{
ServerId = server.Id,
Data = dataNum[(int)property.Order!].ToString(CultureInfo.InvariantCulture),
DataName = $"{property.DisplayName}-{iFace}",
DataType = $"{property.Name}-{iFace}"
};
netWorkDataList.Add(d);
});
totalUsage += dataNum[7];
}
var d = new ServerMonitoringData
{
ServerId = server.Id,
Data = totalUsage.ToString(CultureInfo.InvariantCulture),
DataName = "网络接口总体使用率",
DataType = "InterfaceTotalUtilizationPercentage"
};
netWorkDataList.Add(d);
netWorkDataListAll.AddRange(netWorkDataList);
netWorkDataList.ForEach( Action);
continue;
async void Action(ServerMonitoringData data)
{
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
}
} }
DataHelper.AddMonitoringData(res, totalUsage.ToString(CultureInfo.InvariantCulture), "网络接口的使用率-总", _count++;
"NetWorkUsage"); if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(netWorkDataListAll);
} }
} }

View File

@ -1,34 +1,106 @@
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Models; using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers; using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.SignalR;
using Quartz; using Quartz;
namespace LoongPanel_Asp.Jobs; namespace LoongPanel_Asp.Jobs;
public class ProcessTotalJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class ProcessTotalJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
public async Task Execute(IJobExecutionContext context)
{ {
var output = await sshService.ExecuteCommandAsync(server.Id, "ps", "-e", "|", "wc", "-l"); var dataMap = context.JobDetail.JobDataMap;
if (string.IsNullOrEmpty(output)) return; var dataHelper = new DataHelper(dbContext,hubContext);
var value = int.Parse(output).ToString(); var serverList = (List<ServerModel>)dataMap["executor"];
DataHelper.AddMonitoringData(res, value, "进程总数", "ProcessTotalCount"); var processDataListAll = new List<ServerMonitoringData>();
output = await sshService.ExecuteCommandAsync(server.Id, "ps", "-eLf", "|", "wc", "-l"); var sshClient = serviceProvider.GetService<SshService>();
if (string.IsNullOrEmpty(output)) return; foreach (var server in serverList)
value = int.Parse(output).ToString(); {
DataHelper.AddMonitoringData(res, value, "线程总数", "ThreadTotalCount"); var output = await sshClient?.ExecuteCommandAsync(server.Id, "ps", "-e", "|", "wc", "-l")!;
if (string.IsNullOrEmpty(output)) continue;
var processDataList = new List<ServerMonitoringData>();
var count = int.Parse(output);
var processTotalCount = new ServerMonitoringData
{
ServerId = server.Id,
Data = count.ToString(),
DataName = "进程总数",
DataType = "ProcessTotalCount"
};
processDataList.Add(processTotalCount);
output = await sshClient?.ExecuteCommandAsync(server.Id, "ps", "-eLf", "|", "wc", "-l")!;
if (string.IsNullOrEmpty(output)) continue;
count = int.Parse(output);
var threadsTotalCount = new ServerMonitoringData
{
ServerId = server.Id,
Data = count.ToString(),
DataName = "线程总数",
DataType = "ThreadsTotalCount"
};
processDataList.Add(threadsTotalCount);
processDataList.ForEach(Action);
processDataListAll.AddRange(processDataList);
continue;
async void Action(ServerMonitoringData data)
{
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
}
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(processDataListAll);
} }
} }
public class PhrasePatternJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class PhrasePatternJob(
IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext) : IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res)
public async Task Execute(IJobExecutionContext context)
{ {
var output = await sshService.ExecuteCommandAsync(server.Id, "lsof", "|", "wc", "-l"); var dataMap = context.JobDetail.JobDataMap;
if (string.IsNullOrEmpty(output)) return; var dataHelper = new DataHelper(dbContext,hubContext);
var value = int.Parse(output).ToString(); var serverList = (List<ServerModel>)dataMap["executor"];
DataHelper.AddMonitoringData(res, value, "句柄", "PhrasePatternCount"); var processDataListAll = new List<ServerMonitoringData>();
var sshClient = serviceProvider.GetService<SshService>();
foreach (var server in serverList)
{
var output = await sshClient?.ExecuteCommandAsync(server.Id, "lsof", "|", "wc", "-l")!;
if (string.IsNullOrEmpty(output)) continue;
var count = int.Parse(output);
var phrasePatternCount = new ServerMonitoringData
{
ServerId = server.Id,
Data = count.ToString(),
DataName = "句柄",
DataType = "PhrasePatternCount"
};
processDataListAll.Add(phrasePatternCount);
await hubContext.Clients.All.SendAsync("ReceiveData", server.Id, phrasePatternCount.DataType,
phrasePatternCount.Data);
await dataHelper.CheckData(server.Id, phrasePatternCount.DataType ?? "", phrasePatternCount.Data ?? "",
"句柄数");
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(processDataListAll);
} }
} }

View File

@ -1,18 +0,0 @@
namespace LoongPanel_Asp.Jobs;
// public class SystemTimeIntervalLoadJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService)
// {
// protected override async Task<List<ServerMonitoringData>?> ExecuteInternal(IJobExecutionContext context, ServerModel server, SshService sshService)
// {
// var output = await sshService?.ExecuteCommandAsync(server.Id, "sar", "-u", "3 1", "|", "grep", "Average")!;
// if (string.IsNullOrEmpty(output)) return null;
// var totalUsage = new ServerMonitoringData
// {
// ServerId = server.Id,
// Data = "20",
// DataName = "CPU总使用率",
// DataType = "CpuTotalUsage"
// };
// return new List<ServerMonitoringData> { totalUsage };
// }
// }

View File

@ -1,27 +1,76 @@
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Models; using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers; using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.SignalR;
using Quartz; using Quartz;
namespace LoongPanel_Asp.Jobs; namespace LoongPanel_Asp.Jobs;
public class UserTotalJob(DataService dataService, SshService sshService) : IiJob(dataService, sshService) public class UserTotalJob(IHubContext<SessionHub> hubContext,
IServiceProvider serviceProvider,
ApplicationDbContext dbContext): IJob
{ {
protected override async Task ExecuteInternal(IJobExecutionContext context, ServerModel server, private static int _count;
SshService sshService, List<ServerMonitoringData> res) public async Task Execute(IJobExecutionContext context)
{ {
var output = await sshService.ExecuteCommandAsync(server.Id, // 执行用户统计任务
"ps -o ruser=userForLongName -eo user,pcpu,pmem,comm --sort=-pcpu | awk 'NR>1 && $1 !~ /^systemd/ {user[$1]+=$2; mem[$1]+=$3; count[$1]++; total[$1]=$2+$3} END {for (u in user) print u, user[u], mem[u]/count[u], count[u]}' | sort -k1,1r -k2,2nr"); var dataMap = context.JobDetail.JobDataMap;
if (string.IsNullOrEmpty(output)) return; var dataHelper = new DataHelper(dbContext,hubContext);
var users = output.Split("\n", StringSplitOptions.RemoveEmptyEntries); var serverList = (List<ServerModel>)dataMap["executor"];
var userDataListAll = new List<ServerMonitoringData>();
foreach (var user in users) var sshClient = serviceProvider.GetService<SshService>();
foreach (var server in serverList)
{ {
var values = user.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); var output = await sshClient?.ExecuteCommandAsync(server.Id, "ps -o ruser=userForLongName -eo user,pcpu,pmem,comm --sort=-pcpu | awk 'NR>1 && $1 !~ /^systemd/ {user[$1]+=$2; mem[$1]+=$3; count[$1]++; total[$1]=$2+$3} END {for (u in user) print u, user[u], mem[u]/count[u], count[u]}' | sort -k1,1r -k2,2nr")!;
var name = values[0]; if (string.IsNullOrEmpty(output)) continue;
DataHelper.AddMonitoringData(res, values[1], $"CPU使用率-{name}", $"CpuUsage-{name}"); var lines = output.Split("\n",StringSplitOptions.RemoveEmptyEntries);
DataHelper.AddMonitoringData(res, values[2], $"内存使用率-{name}", $"MemUsage-{name}");
DataHelper.AddMonitoringData(res, values[3], $"用户进程数-{name}", $"UserProcessCount-{name}"); foreach (var line in lines)
{
var d = line.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
var name = d[0];
var cpu = d[1];
var mem = d[2];
var command=d[3];
var data = new ServerMonitoringData
{
ServerId = server.Id,
Data = cpu,
DataName = $"CPU使用率-{name}",
DataType = $"CpuUsage-{name}"
};
userDataListAll.Add(data);
data = new ServerMonitoringData
{
ServerId = server.Id,
Data = mem,
DataName = $"内存使用率-{name}",
DataType = $"MemoryUsage-{name}"
};
userDataListAll.Add(data);
data = new ServerMonitoringData
{
ServerId = server.Id,
Data = command,
DataName = $"用户进程数-{name}",
DataType = $"UserProcesses-{name}"
};
userDataListAll.Add(data);
}
}
userDataListAll.ForEach(Action);
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(userDataListAll);
return;
async void Action(ServerMonitoringData data)
{
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
await dataHelper.CheckData(data.ServerId, data.DataType ?? "", data.Data ?? "", data.DataName);
} }
} }
} }

View File

@ -18,8 +18,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6"/>
<PackageReference Include="MimeKit" Version="4.6.0"/> <PackageReference Include="MimeKit" Version="4.6.0"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
<PackageReference Include="Quartz" Version="3.9.0"/> <PackageReference Include="Quartz" Version="3.9.0"/>
<PackageReference Include="Quartz.AspNetCore" Version="3.9.0"/> <PackageReference Include="Quartz.AspNetCore" Version="3.9.0"/>
@ -27,7 +26,7 @@
<PackageReference Include="SSH.NET" Version="2024.0.0"/> <PackageReference Include="SSH.NET" Version="2024.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.0"/> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.0"/>
<PackageReference Include="System.Linq.Async" Version="6.0.1"/> <PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -36,20 +35,23 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="Configs\servers.ini"> <None Update="Configs\servers.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="markdowns\templates\巡检模板1.md"> <None Update="markdowns\templates\巡检模板1.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="app.db">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Configs\Alerts\"/> <Folder Include="Configs\Alerts\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Update="wwwroot\index"> <Content Update="wwwroot\index">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -16,13 +16,13 @@ public class ApiPermissionMiddleware(
// 获取配置中定义的公开API列表 // 获取配置中定义的公开API列表
var publicApis = configuration["PublicApi"]?.Split(";", StringSplitOptions.RemoveEmptyEntries) ?? var publicApis = configuration["PublicApi"]?.Split(";", StringSplitOptions.RemoveEmptyEntries) ??
[]; [];
if (publicApis.Any(api => context.Request.Path.Value == api)) if (publicApis.Any(api => context.Request.Path.Value == api))
{ {
await next(context); await next(context);
return; return;
} }
if (context.Request.Path.Value!.StartsWith("/public")) if (context.Request.Path.Value!.StartsWith("/public"))
{ {
await next(context); await next(context);
@ -84,10 +84,14 @@ public class ApiPermissionMiddleware(
// 判断请求是否拥有权限 // 判断请求是否拥有权限
if (!apiPermissions.Any(x => if (!apiPermissions.Any(x =>
context.Request.Path.Value != null && context.Request.Path.Value.StartsWith(x))) context.Request.Path.Value != null && context.Request.Path.Value.StartsWith(x)))
{
await ForbiddenResponse(context); await ForbiddenResponse(context);
}
else else
{
// 请求拥有权限,调用下一个中间件 // 请求拥有权限,调用下一个中间件
await next(context); await next(context);
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -18,7 +18,6 @@ public class PermissionMiddleware(
await next(context); await next(context);
return; return;
} }
Console.WriteLine(context.Request.Path.Value!); Console.WriteLine(context.Request.Path.Value!);
//如果访问 /public/* //如果访问 /public/*
if (context.Request.Path.Value!.StartsWith("/public")) if (context.Request.Path.Value!.StartsWith("/public"))
@ -26,7 +25,7 @@ public class PermissionMiddleware(
await next(context); await next(context);
return; return;
} }
// 获取请求头中的Authorization信息 // 获取请求头中的Authorization信息
var authorizationHeader = context.Request.Headers["Authorization"]; var authorizationHeader = context.Request.Headers["Authorization"];

View File

@ -5,6 +5,7 @@ public class EmailModel
public required string Email { get; set; } public required string Email { get; set; }
} }
public class RegisterModel : EmailModel public class RegisterModel : EmailModel
{ {
public required string UserName { get; set; } public required string UserName { get; set; }

View File

@ -7,9 +7,10 @@ public class Property
public int? Order { get; set; } public int? Order { get; set; }
} }
public class ServerModel public class ServerModel
{ {
public required string Id { get; init; } public required string Id { get;init; }
public required string Address { get; set; } public required string Address { get; set; }
public required int Port { get; set; } public required int Port { get; set; }
public required string ServerName { get; set; } public required string ServerName { get; set; }
@ -18,10 +19,11 @@ public class ServerModel
public required bool Http { get; set; } public required bool Http { get; set; }
} }
public class EmailSettings public class EmailSettings
{ {
public required string Host { get; init; } public required string Host {get;init;}
public required string Port { get; init; } public required string Port {get;init;}
public required string Username { get; init; } public required string Username {get;init;}
public required string Password { get; init; } public required string Password {get;init;}
} }

View File

@ -3,10 +3,11 @@ using LoongPanel_Asp;
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs; using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Middlewares; using LoongPanel_Asp.Middlewares;
using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers; using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NLog.Web;
using Quartz; using Quartz;
using Quartz.AspNetCore; using Quartz.AspNetCore;
@ -75,9 +76,8 @@ builder.Services.AddCors(options =>
policy => policy =>
{ {
//允许全部 //允许全部
policy.WithOrigins("http://localhost:3001", "http://192.168.1.22:3001", "http://192.168.1.22:3001", policy.WithOrigins("http://localhost:3001", "http://localhost:3000","https://localhost:3000","http://127.0.0.1:3000","http://192.168.1.22:3001", "http://192.168.1.22:3001",
"https://192.168.0.22:3000", "https://loongpanel.xn--7p0a.site").AllowAnyHeader().AllowAnyMethod() "https://192.168.0.22:3000","https://loongpanel.xn--7p0a.site").AllowAnyHeader().AllowAnyMethod().AllowCredentials();
.AllowCredentials();
}); });
}); });
@ -104,12 +104,12 @@ builder.Services.AddQuartzServer(options =>
builder.Services.AddScoped<SshService>(); builder.Services.AddScoped<SshService>();
builder.Services.AddSingleton<SshStreamService>(); builder.Services.AddSingleton<SshStreamService>();
builder.Services.AddSingleton<DataService>();
builder.Services.AddHostedService<Init>(); builder.Services.AddHostedService<Init>();
builder.Logging.ClearProviders();
builder.Host.UseNLog();
var app = builder.Build(); var app = builder.Build();

View File

@ -1,71 +0,0 @@
using System.Globalization;
using LoongPanel_Asp.Hubs;
using Microsoft.AspNetCore.SignalR;
namespace LoongPanel_Asp.Servers;
public class DataService(IServiceProvider serviceProvider, IHubContext<SessionHub> context,ILogger<DataService> logger) : IDisposable
{
//创建一个存储
private static readonly Dictionary<string, ServerMonitoringData> ServerMonitoringData = new();
public void Dispose()
{
Submit().Wait();
}
public async Task Save(ServerMonitoringData data)
{
data.Time = DateTime.UtcNow;
//取两位小数
data.Data = Math.Round(double.Parse(data.Data!), 2).ToString(CultureInfo.InvariantCulture);
ServerMonitoringData[$"{data.DataType}-{data.ServerId}"] = data;
logger.LogInformation($"发送数据{data.DataType}:{data.Data}成功");
await context.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
}
public async Task Save(List<ServerMonitoringData> datas)
{
datas.ForEach(x =>
{
x.Time = DateTime.UtcNow;
x.Data = Math.Round(double.Parse(x.Data!), 2).ToString(CultureInfo.InvariantCulture);
});
foreach (var data in datas)
{
ServerMonitoringData[$"{data.DataType}-{data.ServerId}"] = data;
logger.LogInformation($"发送数据{data.DataType}:{data.Data}成功");
await context.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
}
}
//提交
public async Task Submit()
{
try
{
using (var scope = serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var dataDb = dbContext.ServerMonitoringData;
await dataDb.AddRangeAsync(ServerMonitoringData.Values);
await dbContext.SaveChangesAsync();
logger.LogInformation($"监控数据已保存,共保存 {ServerMonitoringData.Count} 条记录。");
Console.WriteLine($"监控数据已保存,共保存 {ServerMonitoringData.Count} 条记录。");
}
ServerMonitoringData.Clear();
}
catch (Exception ex)
{
// 记录异常信息
logger.LogError(ex, "保存监控数据时发生错误。");
// 根据需要重新抛出异常或进行其他错误处理
Console.WriteLine(ex.Message);
throw;
}
}
private async Task CheckData()
{
}
}

View File

@ -1,61 +1,54 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Net;
using LoongPanel_Asp.Models;
using MimeKit; using MimeKit;
using SmtpClient = MailKit.Net.Smtp.SmtpClient; using SmtpClient = MailKit.Net.Smtp.SmtpClient;
namespace LoongPanel_Asp.Servers; namespace LoongPanel_Asp.Servers;
public class EmailService : IDisposable public class EmailService:IDisposable
{ {
private readonly IConfigurationSection _emailSettings;
private readonly SmtpClient _smtpClient; private readonly SmtpClient _smtpClient;
private readonly IConfigurationSection _emailSettings;
//创建一个验证码缓存字典 //创建一个验证码缓存字典
private readonly ConcurrentDictionary<string, string> _verifyCodeCache = new(); private readonly ConcurrentDictionary<string, string> _verifyCodeCache = new();
public EmailService(IConfigurationSection emailSettings) public EmailService(IConfigurationSection emailSettings)
{ {
_emailSettings = emailSettings; _emailSettings = emailSettings;
// 初始化SmtpClient // 初始化SmtpClient
_smtpClient = new SmtpClient(); _smtpClient = new SmtpClient();
_smtpClient.Connect(_emailSettings["Host"], _smtpClient.Connect(_emailSettings["Host"], int.Parse(_emailSettings["Port"] ?? throw new InvalidOperationException()), true);
int.Parse(_emailSettings["Port"] ?? throw new InvalidOperationException()), true);
_smtpClient.Authenticate(_emailSettings["Username"], _emailSettings["Password"]); _smtpClient.Authenticate(_emailSettings["Username"], _emailSettings["Password"]);
} }
public void Dispose()
{
_smtpClient.Dispose();
}
//发送邮件 //发送邮件
public async Task SendEmailAsync(string toAddress, string toName, string subject, string body) public async Task SendEmailAsync(string toAddress,string toName, string subject, string body)
{ {
using var mailMessage = new MimeMessage(); using var mailMessage = new MimeMessage();
mailMessage.From.Add(new MailboxAddress("龙盾云御", _emailSettings["Username"])); mailMessage.From.Add(new MailboxAddress("龙盾云御",_emailSettings["Username"]));
mailMessage.To.Add(new MailboxAddress(toName, toAddress)); mailMessage.To.Add(new MailboxAddress(toName, toAddress));
mailMessage.Subject = subject; mailMessage.Subject = subject;
var bodyBuilder = new BodyBuilder var bodyBuilder = new BodyBuilder
{ {
HtmlBody = body HtmlBody = body,
}; };
mailMessage.Body = bodyBuilder.ToMessageBody(); mailMessage.Body = bodyBuilder.ToMessageBody();
await _smtpClient.SendAsync(mailMessage); await _smtpClient.SendAsync(mailMessage);
} }
//发送邮箱验证码 //发送邮箱验证码
public async Task SendEmailVerifyCodeAsync(string userId, string toAddress, string toName) public async Task SendEmailVerifyCodeAsync( string userId,string toAddress, string toName)
{ {
const string subject = "龙盾云御邮箱验证码"; const string subject = "龙盾云御邮箱验证码";
var code = GenerateVerifyCode(); var code=GenerateVerifyCode();
var body = $"<h1>您的验证码是:{code}</h1>"; var body = $"<h1>您的验证码是:{code}</h1>";
await SendEmailAsync(toAddress, toName, subject, body); await SendEmailAsync(toAddress, toName, subject, body);
//存入缓存 //存入缓存
_verifyCodeCache.AddOrUpdate(userId, code, (key, oldValue) => code); _verifyCodeCache.AddOrUpdate(userId, code, (key, oldValue) => code);
} }
//验证邮箱验证码 //验证邮箱验证码
public bool VerifyEmailVerifyCode(string userId, string code) public bool VerifyEmailVerifyCode(string userId, string code)
{ {
@ -64,8 +57,8 @@ public class EmailService : IDisposable
_verifyCodeCache.TryRemove(userId, out _); _verifyCodeCache.TryRemove(userId, out _);
return true; return true;
} }
//生成6位随机验证码 //生成6位随机验证码
private static string GenerateVerifyCode() private static string GenerateVerifyCode()
{ {
@ -73,4 +66,9 @@ public class EmailService : IDisposable
var verifyCode = random.Next(100000, 999999).ToString(); var verifyCode = random.Next(100000, 999999).ToString();
return verifyCode; return verifyCode;
} }
public void Dispose()
{
throw new NotImplementedException();
}
} }

View File

@ -1,4 +1,5 @@
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Models;
using Renci.SshNet; using Renci.SshNet;
using ConnectionInfo = Renci.SshNet.ConnectionInfo; using ConnectionInfo = Renci.SshNet.ConnectionInfo;
@ -9,8 +10,6 @@ public class SshService : IDisposable
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
private readonly ApplicationDbContext _db; private readonly ApplicationDbContext _db;
private readonly ILogger<SshService> _logger; private readonly ILogger<SshService> _logger;
//注册nlog
private readonly Dictionary<string, ConnectionInfo> _serverConnectionInfos = new(); private readonly Dictionary<string, ConnectionInfo> _serverConnectionInfos = new();
private readonly Dictionary<string, SshClient> _serverSshClients = new(); private readonly Dictionary<string, SshClient> _serverSshClients = new();
@ -31,13 +30,12 @@ public class SshService : IDisposable
private void LoadServerConfigurations() private void LoadServerConfigurations()
{ {
// 从数据库中加载服务器配置信息 // 从数据库中加载服务器配置信息
var serverConfigurations = JobConfigHelper.GetServers(); var serverConfigurations=JobConfigHelper.GetServers();
foreach (var serverConfiguration in serverConfigurations) foreach (var serverConfiguration in serverConfigurations)
{ {
var connectionInfo = new ConnectionInfo(serverConfiguration.Address, serverConfiguration.Port, var connectionInfo = new ConnectionInfo(serverConfiguration.Address, serverConfiguration.Port,
serverConfiguration.Username, serverConfiguration.Username, new PasswordAuthenticationMethod(serverConfiguration.Username, serverConfiguration.Password));
new PasswordAuthenticationMethod(serverConfiguration.Username, serverConfiguration.Password));
_serverConnectionInfos[serverConfiguration.Id] = connectionInfo; _serverConnectionInfos[serverConfiguration.Id] = connectionInfo;
var sshClient = new SshClient(connectionInfo); var sshClient = new SshClient(connectionInfo);
// 设置超时时间 // 设置超时时间
@ -46,39 +44,30 @@ public class SshService : IDisposable
} }
} }
public async Task<string?> ExecuteCommandAsync(string serverId, string command, params string[] arguments) public async Task<string> ExecuteCommandAsync(string serverId, string command, params string[] arguments)
{ {
if (!_serverSshClients.TryGetValue(serverId, out var sshClient)) var sshClient = _serverSshClients[serverId];
{ if (sshClient == null) throw new InvalidOperationException($"SSH client for server ID '{serverId}' not found.");
var errorMessage = $"服务器ID '{serverId}' 的SSH客户端未找到。";
_logger.LogError(errorMessage);
throw new InvalidOperationException(errorMessage);
}
var output = ""; var output = "";
try try
{ {
// 确保在执行命令前连接到服务器 // 确保在执行命令前连接到服务器
if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token); if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token);
var commandString = string.Join(" ", "LANG=C", command, string.Join(" ", arguments)); var commandString = string.Join(" ", "LANG=C", command, string.Join(" ", arguments));
_logger.LogInformation($"服务器ID{serverId},正在执行命令:{commandString}"); Console.WriteLine(commandString);
using var commandResult = sshClient.RunCommand(commandString); using var commandResult = sshClient.RunCommand(commandString);
output = commandResult.Result; output = commandResult.Result;
if (commandResult.ExitStatus != 0) if (commandResult.ExitStatus != 0) output = commandResult.Error;
{
_logger.LogError($"命令 {commandString} 执行失败,退出状态码 {commandResult.ExitStatus}\n {output}");
return null;
}
_logger.LogInformation($"命令 {commandString} 执行成功:{output}");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "执行命令时发生错误。"); Console.WriteLine(ex);
} }
return output; return output;
} }
public async Task<string> ExecuteCommandAsync(bool langC, string serverId, string command, public async Task<string> ExecuteCommandAsync(bool langC, string serverId, string command,
params string[] arguments) params string[] arguments)
{ {

View File

@ -1,25 +1,95 @@
using System.Collections.Concurrent; using Renci.SshNet;
using System;
using System.Collections.Concurrent;
using System.IO;
using LoongPanel_Asp.Helpers; using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs; using LoongPanel_Asp.Hubs;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Renci.SshNet;
using ConnectionInfo = Renci.SshNet.ConnectionInfo; using ConnectionInfo = Renci.SshNet.ConnectionInfo;
namespace LoongPanel_Asp.Servers; namespace LoongPanel_Asp.Servers;
public class SshStreamService : IDisposable public class SshStreamService : IDisposable
{ {
private readonly IHubContext<TerminalHub> _hubContext;
private readonly ConcurrentDictionary<string, (SshClient Client, ShellStream Stream)> _sshStreams = new(); private readonly ConcurrentDictionary<string, (SshClient Client, ShellStream Stream)> _sshStreams = new();
private readonly CancellationTokenSource _cancellationTokenSource; private readonly IHubContext<TerminalHub> _hubContext;
private readonly Task _readingTask; private CancellationTokenSource _cancellationTokenSource;
private Task _readingTask;
public SshStreamService(IHubContext<TerminalHub> hubContext) public SshStreamService(IHubContext<TerminalHub> hubContext)
{ {
_hubContext = hubContext; _hubContext = hubContext;
_cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource = new CancellationTokenSource();
_readingTask = Task.Run(() => StartReadingAsync(_cancellationTokenSource.Token)); _readingTask = Task.Run(() => StartReadingAsync(_cancellationTokenSource.Token));
} }
public void Connect(string userId, string serverId)
{
// 假设 JobConfigHelper.GetServers() 返回一个包含服务器配置的列表
var serverConfig = JobConfigHelper.GetServers().Find(x => x.Id == serverId);
if (serverConfig == null) throw new InvalidOperationException("Server not found.");
//从_sshStreams查找是否存在 替换
var host = serverConfig.Address;
var port = serverConfig.Port;
var username = serverConfig.Username;
var password = serverConfig.Password;
var connectionInfo = new ConnectionInfo(host, port, username, new PasswordAuthenticationMethod(username, password));
var sshClient = new SshClient(connectionInfo);
sshClient.Connect();
var shellStream = sshClient.CreateShellStream("xterm", 100, 40, 800, 600, 1024);
if (_sshStreams.TryGetValue(userId, out var existing))
{
// 关闭旧的连接
existing.Client.Disconnect();
existing.Client.Dispose();
existing.Stream.Close();
existing.Stream.Dispose();
}
_sshStreams[userId]= (sshClient, shellStream);
}
public void ReSize(string userId,int col, int row)
{
//从sshStreams中获取对象
if (_sshStreams.TryGetValue(userId, out var tuple))
{
var (ssh, stream) = tuple;
if (ssh.IsConnected)
{
var ansiResizeCommand = $"stty cols {col}; stty rows {row}";
stream.WriteLine(ansiResizeCommand);
}
else
{
throw new InvalidOperationException("SSH client is not connected or shell stream is not available.");
}
}
else
{
throw new InvalidOperationException("SSH connection not found for the given user ID.");
}
}
public void Write(string userId, string data)
{
if (!_sshStreams.TryGetValue(userId, out var sshStreamInfo)) return;
var (_, shellStream) = sshStreamInfo;
shellStream?.Write(data);
}
public void Disconnect(string userId)
{
if (_sshStreams.TryRemove(userId, out var sshStreamInfo))
{
var (sshClient, shellStream) = sshStreamInfo;
shellStream?.Close();
shellStream?.Dispose();
sshClient.Disconnect();
sshClient.Dispose();
}
}
public void Dispose() public void Dispose()
{ {
@ -41,80 +111,9 @@ public class SshStreamService : IDisposable
sshClient.Disconnect(); sshClient.Disconnect();
sshClient.Dispose(); sshClient.Dispose();
} }
_sshStreams.Clear(); _sshStreams.Clear();
} }
public void Connect(string userId, string serverId)
{
// 假设 JobConfigHelper.GetServers() 返回一个包含服务器配置的列表
var serverConfig = JobConfigHelper.GetServers().Find(x => x.Id == serverId);
if (serverConfig == null) throw new InvalidOperationException("Server not found.");
//从_sshStreams查找是否存在 替换
var host = serverConfig.Address;
var port = serverConfig.Port;
var username = serverConfig.Username;
var password = serverConfig.Password;
var connectionInfo =
new ConnectionInfo(host, port, username, new PasswordAuthenticationMethod(username, password));
var sshClient = new SshClient(connectionInfo);
sshClient.Connect();
var shellStream = sshClient.CreateShellStream("xterm", 100, 40, 800, 600, 1024);
if (_sshStreams.TryGetValue(userId, out var existing))
{
// 关闭旧的连接
existing.Client.Disconnect();
existing.Client.Dispose();
existing.Stream.Close();
existing.Stream.Dispose();
}
_sshStreams[userId] = (sshClient, shellStream);
}
public void ReSize(string userId, int col, int row)
{
//从sshStreams中获取对象
if (_sshStreams.TryGetValue(userId, out var tuple))
{
var (ssh, stream) = tuple;
if (ssh.IsConnected)
{
var ansiResizeCommand = $"stty cols {col}; stty rows {row}";
stream.WriteLine(ansiResizeCommand);
}
else
{
throw new InvalidOperationException("SSH client is not connected or shell stream is not available.");
}
}
else
{
throw new InvalidOperationException("SSH connection not found for the given user ID.");
}
}
public void Write(string userId, string data)
{
if (!_sshStreams.TryGetValue(userId, out var sshStreamInfo)) return;
var (_, shellStream) = sshStreamInfo;
shellStream?.Write(data);
}
public void Disconnect(string userId)
{
if (_sshStreams.TryRemove(userId, out var sshStreamInfo))
{
var (sshClient, shellStream) = sshStreamInfo;
shellStream?.Close();
shellStream?.Dispose();
sshClient.Disconnect();
sshClient.Dispose();
}
}
private async Task StartReadingAsync(CancellationToken cancellationToken) private async Task StartReadingAsync(CancellationToken cancellationToken)
{ {
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)

View File

@ -1,26 +1,26 @@
| ֵ<EFBFBD><EFBFBD><EFBFBD>¼ | col | col | col | col | col | | Öµ°à¼Ç¼ | col | col | col | col | col |
|---------|---------|---------|---------|---------|---------| | -------- | ------- | ------- | ------- | ------- | ------- |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |
| content | content | content | content | content | content | | content | content | content | content | content | content |

View File

@ -8,21 +8,21 @@
## 第二部分:巡检项目 ## 第二部分:巡检项目
| 序号 | 巡检项目名称 | 巡检要点 | 巡检内容 | | 序号 | 巡检项目名称 | 巡检要点 | 巡检内容 |
|-----|--------|---------------------------|---------------------------------| | ---- | ------------ | -------------------------------------------------- | -------------------------------------------------------------- |
| 1 | 设备运行状况 | 检查设备运行是否平稳,有无异常振动或噪音。 | 详细记录设备运行参数,比较历史数据,分析是否存在异常。 | | 1 | 设备运行状况 | 检查设备运行是否平稳,有无异常振动或噪音。 | 详细记录设备运行参数,比较历史数据,分析是否存在异常。 |
| 2 | 安全防护措施 | 检查安全防护设施是否完好,如安全栅栏、警示标志等。 | 确认所有安全设施无损坏,位置正确,且工作人员了解如何正确使用。 | | 2 | 安全防护措施 | 检查安全防护设施是否完好,如安全栅栏、警示标志等。 | 确认所有安全设施无损坏,位置正确,且工作人员了解如何正确使用。 |
| 3 | 环境卫生状况 | 检查工作区域是否清洁,有无垃圾或障碍物。 | 清理工作区域,确保无杂物,保持环境整洁。 | | 3 | 环境卫生状况 | 检查工作区域是否清洁,有无垃圾或障碍物。 | 清理工作区域,确保无杂物,保持环境整洁。 |
| ... | ..... | .... | ... | |...|.....|....|...|
## 第三部分:检查记录 ## 第三部分:检查记录
| 序号 | 检查项目 | 检查结果 | 异常说明 | 处理措施 | 反馈意见 | | 序号 | 检查项目 | 检查结果 | 异常说明 | 处理措施 | 反馈意见 |
|-----|-------|------|----------|-----------------|-------------| | ---- | -------- | -------- | ---------------- | ------------------------------ | ---------------------- |
| 1 | 外壳 | 正常 | 无 | 无 | 无 | | 1 | 外壳 | 正常 | 无 | 无 | 无 |
| 2 | 电源 | 异常 | 设备有轻微振动。 | 已联系维修人员,计划明日检修。 | 建议增加设备维护频率。 | | 2 | 电源 | 异常 | 设备有轻微振动。 | 已联系维修人员,计划明日检修。 | 建议增加设备维护频率。 |
| 3 | 主机 | 正常 | 无 | 无 | 无 | | 3 | 主机 | 正常 | 无 | 无 | 无 |
| ... | ..... | .... | ... | ... | ... | |...|.....|....|...|...|...|
## 第四部分:巡检总结 ## 第四部分:巡检总结

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Info"
internalLogFile="c:\temp\internal-nlog-AspNetCore.txt">
<!-- 启用 ASP.NET Core 布局渲染器 -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<!-- 日志写入的目标 -->
<targets>
<!-- 所有日志消息的基础详细信息文件目标 -->
<target xsi:type="File" name="allfile" fileName="logs/all-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message}${exception:format=tostring}" />
<!-- 自定义日志消息的额外Web详细信息文件目标 -->
<target xsi:type="File" name="ownFile-web" fileName="logs/own-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message}${exception:format=tostring}|url: ${aspnet-request-url}|action:${aspnet-mvc-action}" />
<!-- 控制台目标,用于提高 Docker / Visual Studio 启动检测的速度 -->
<target xsi:type="Console" name="lifetimeConsole" layout="${MicrosoftConsoleLayout}" />
<!-- LoongPanel_Asp.Servers.SshService 日志的单独文件目标 -->
<target xsi:type="File" name="sshServiceFile" fileName="logs/ssh-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message}${exception:format=tostring}" />
</targets>
<!-- 将日志记录器名称映射到目标的规则 -->
<rules>
<!-- 所有日志,包括来自 Microsoft 的 -->
<logger name="*" minlevel="Trace" writeTo="allfile" />
<!-- 将宿主生命周期消息输出到控制台目标,以便更快地启动检测 -->
<logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="lifetimeConsole, ownFile-web" final="true" />
<!-- 忽略非关键的 Microsoft 日志,因此只记录自己的日志 -->
<logger name="Microsoft.*" maxlevel="Info" final="true" />
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
<!-- 将 LoongPanel_Asp.Servers.SshService 的日志消息写入单独的文件 -->
<logger name="LoongPanel_Asp.Servers.SshService" minlevel="Trace" writeTo="sshServiceFile" final="true" />
<logger name="*" minlevel="Trace" writeTo="ownFile-web" />
</rules>
</nlog>

View File

@ -1 +1 @@
NUXT_API_URL="https://backend.xn--7p0a.site" NUXT_API_URL="127.0.0.1:5000"

View File

@ -1 +0,0 @@
NUXT_API_URL="http://192.168.1.22:5000"

1
web/.env.yuan Normal file
View File

@ -0,0 +1 @@
NUXT_API_URL="https://backend.xn--7p0a.site"

1
web/.gitignore vendored
View File

@ -1,5 +1,6 @@
# Nuxt dev/build outputs # Nuxt dev/build outputs
.output .output
output
.data .data
.nuxt .nuxt
.nitro .nitro

View File

@ -3,6 +3,7 @@ import {charts} from "~/config/charts";
import type {PropType} from "vue"; import type {PropType} from "vue";
import type {serverValueItem} from "~/components/SettingCard.vue"; import type {serverValueItem} from "~/components/SettingCard.vue";
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore"; import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import {THEME_KEY} from "vue-echarts";
const mainLayoutStore = useMainLayoutStore() const mainLayoutStore = useMainLayoutStore()
@ -94,15 +95,25 @@ const items = [
<template> <template>
<div :id="`card_${id}`" ref="cardRef" class="card-layout "> <div :id="`card_${id}`" ref="cardRef" class="card-layout ">
<Dialog v-model:visible="visible" :pt="{
root: 'border-none',
mask: {
style: 'backdrop-filter: blur(10px)'
}
}" modal>
<template #container="{ closeCallback }">
<SettingCard :card-id="id" :close-callback="closeCallback" is-setting/>
</template>
</Dialog>
<SplitButton :model="items" class="SplitButton"/>
<div :id="'item' + id" class="card-title vue-draggable-handle"> <div :id="'item' + id" class="card-title vue-draggable-handle">
<div></div>
<h3>{{ title ?? "默认标题" }}</h3> <h3>{{ title ?? "默认标题" }}</h3>
</div> </div>
<div class="card-content"> <component :is="charts.find(x => x.id === chart)?.component" v-if="targetIsVisible&&!isSwitched"
<component :is="charts.find(x => x.id === chart)?.component" v-if="targetIsVisible&&!isSwitched" :rangeNum="rangeNum"
:rangeNum="rangeNum" :valueIds="valueIds"
:valueIds="valueIds" :valueNames="valueNames"/>
:valueNames="valueNames"/>
</div>
</div> </div>
</template> </template>
@ -110,31 +121,71 @@ const items = [
@import "base"; @import "base";
.card-layout { .card-layout {
height: 100%;
width: 100%; width: 100%;
height: 100%;
background: $light-bg-color; background: $light-bg-color;
display: grid; border-radius: $radius;
grid-template-rows: 40px 1fr; box-shadow: 0 10px 30px 0 rgba(17, 38, 146, 0.05);
padding: $padding*1.5;
position: relative;
display: flex;
flex-direction: column;
gap: $gap*1.5;
will-change: scroll-position, contents;
border: $border;
.dark-mode & { .dark-mode & {
background: $dark-bg-color; background: $dark-bg-color;
} }
} }
.card-title { .SplitButton {
display: flex; position: absolute;
justify-content: space-between; right: $padding*.5;
align-items: center; top: $padding*.5;
padding: 0 20px;
:deep(.p-button) {
h3 { background: unset;
font-size: 14px; border: unset;
line-height: 24px; padding: unset;
text-align: left;
color: #333; > svg {
stroke: $light-unfocused-color;
.dark-mode & {
stroke: $dark-unfocused-color;
}
&:hover {
stroke: $light-text-color;
.dark-mode & {
stroke: $dark-text-color;
}
}
}
} }
} }
.card-content{
padding: 0 20px 20px; .card-title {
display: flex;
align-items: center;
gap: $gap;
color: $light-text-color;
&,
h3:hover {
cursor: move;
}
.dark-mode & {
color: $dark-text-color;
}
div {
width: 6px;
height: 20px;
border-radius: $radius;
background: $primary-color;
}
} }
</style> </style>

View File

@ -7,6 +7,10 @@ const props = defineProps({
type: String, type: String,
default: "CPU使用率", default: "CPU使用率",
}, },
info:{
type:Array,
default:()=>[],
},
unit: { unit: {
type: String, type: String,
default: "%" default: "%"
@ -46,7 +50,7 @@ dataStore.$subscribe((_, state) => {
<template #trigger> <template #trigger>
<div class="mini-card-box"> <div class="mini-card-box">
<n-progress type="circle" processing :status="status" :percentage=" <n-progress type="circle" processing :status="status" :percentage="
Number(values[0]) values[0]
" style="width: 68px;"> " style="width: 68px;">
<MoveDown class="arrow" :style="{transform:`rotate(${(100-value)/100*360*-1}deg)`}" v-if="status==='success'"/> <MoveDown class="arrow" :style="{transform:`rotate(${(100-value)/100*360*-1}deg)`}" v-if="status==='success'"/>
<div class="warning" v-if="status==='warning'"> <div class="warning" v-if="status==='warning'">
@ -66,9 +70,9 @@ dataStore.$subscribe((_, state) => {
</div> </div>
</div> </div>
</template> </template>
<slot> <n-tag type="info" v-for="i in info">
这里什么都没有 {{ i }}
</slot> </n-tag>
</n-popover> </n-popover>
</template> </template>

View File

@ -4,14 +4,17 @@ import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import type {PropType} from "vue"; import type {PropType} from "vue";
import {ref} from 'vue'; import {ref} from 'vue';
import dayjs from "dayjs";
import VChart from 'vue-echarts'; import VChart from 'vue-echarts';
import _ from "lodash"; import _ from "lodash";
import {useSessionSignalRStore} from "~/strores/HubStore";
const dataStore = useDataStore() const dataStore = useDataStore()
const hubStore = useSessionSignalRStore()
const mainLayoutStore = useMainLayoutStore() const mainLayoutStore = useMainLayoutStore()
const chartRef = ref<any>(null) type ArbitraryKeyValuePairs = {
[key: string]: (number | string)[];
};
const values = ref<ArbitraryKeyValuePairs>({});
const chartRet = ref<any>(null)
const isLoading = ref<boolean>(true) const isLoading = ref<boolean>(true)
const props = defineProps({ const props = defineProps({
valueIds: { valueIds: {
@ -29,9 +32,7 @@ const props = defineProps({
}) })
const option = computed(() => { const option = computed(() => {
return { return {
backgroundColor: '', backgroundColor:'',
//
// animation: false,
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
@ -42,39 +43,47 @@ const option = computed(() => {
} }
}, },
grid: { grid: {
left: '0', left: '2%',
right: '50', right: '50',
bottom: '38', bottom: '10%',
top: '15',
containLabel: true containLabel: true
}, },
xAxis: [ xAxis: [
{ {
type: 'time', type: 'category',
boundaryGap: false, boundaryGap: false,
axisLabel: { data: []
showMinLabel: true, }
showMaxLabel: true,
}
},
], ],
yAxis: [ yAxis: [
{ {
type: 'value', type: 'value',
} }
], ],
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
saveAsImage: {}
}
},
dataZoom: [ dataZoom: [
{ {
type: 'inside', type: 'inside',
start: 99.5, start: 90,
end: 100, end: 100,
}, },
{ {
type: 'slider', type: 'slider',
throttle: 500,
}, },
{ {
type: 'slider', type: 'slider',
yAxisIndex: 0, yAxisIndex: 0,
startValue: 0,
endValue: 100,
}, },
], ],
legend: { legend: {
@ -87,20 +96,9 @@ const option = computed(() => {
areaStyle: {}, areaStyle: {},
large: true, large: true,
smooth: true, smooth: true,
largeThreshold: 10000,
sampling: 'lttb',
emphasis: { emphasis: {
focus: 'series' focus: 'series'
}, },
markPoint: {
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
markLine: {
data: [{type: 'average', name: 'Avg'}]
},
data: [], data: [],
} }
}) ?? [] }) ?? []
@ -110,34 +108,59 @@ let interval: NodeJS.Timeout;
onUnmounted(() => { onUnmounted(() => {
clearInterval(interval); clearInterval(interval);
}) })
let maxValue = 100;
onMounted(() => { onMounted(() => {
if (!chartRef.value) { let history = dataStore.dataHistory
console.error('ECharts instance is not initialized');
return;
}
hubStore.connection?.on("ReceiveDataHistory", (type: string, values: [number,string][], done: boolean) => {
chartRef.value.appendData({
seriesIndex: props.valueIds?.indexOf(type),
data: values
})
if (done) {
//y140%
// const currentOption = chartRef.value.getOption();
// const maxData = Math.max(...values.map(item =>item[1]));
// if (maxData > maxValue) {
// maxValue = maxData
// currentOption.yAxis[0].max = maxData * 1.4;
// chartRef.value.setOption(currentOption);
// }
}
isLoading.value = false;
})
setTimeout(() => { setTimeout(() => {
isLoading.value = false;
}, 5000)
nextTick(() => {
props.valueIds?.forEach((id, index) => {
chartRet.value.appendData({
seriesIndex: index,
data: history.data[id]
})
})
const currentOption = chartRet.value.getOption();
currentOption.xAxis[0].data = history.times
chartRet.value.setOption(currentOption)
isLoading.value = false
interval = setInterval(() => {
const data = dataStore.data
props.valueIds?.forEach((id, index) => {
const newData = data[id] ?? 0
if (!values.value[id]) {
values.value[id] = []
}
chartRet.value.appendData({
seriesIndex: index,
data: [newData]
})
})
const currentOption = chartRet.value.getOption();
currentOption.xAxis[0].data.push(dayjs().format('MM-DD HH:mm:ss'))
chartRet.value.setOption(currentOption)
}, 4000 + 1000 * Math.random())
})
})
let endIndex = dataStore.dataHistory.times ? dataStore.dataHistory.times.length : 0
let startTemp = 0;
let done = false;
type Data = {
times: string[]; //
data: { [key: string]: string[] }; //
endIndex: number; //
done: boolean; //
};
const zoom = _.throttle((e: any) => {
console.log(e)
let start = e.start ?? e.batch[0].start
if (done) return
if (start <= 50 && start !== startTemp) {
startTemp = e.start
isLoading.value = true
setTimeout(() => {
isLoading.value = false
}, 5000)
$fetch('/Api/Server/GetServerHistoryDate', { $fetch('/Api/Server/GetServerHistoryDate', {
method: 'GET', method: 'GET',
headers: { headers: {
@ -146,75 +169,41 @@ onMounted(() => {
params: { params: {
ServerId: mainLayoutStore.SelectServer.value, ServerId: mainLayoutStore.SelectServer.value,
DataTypes: props.valueIds, DataTypes: props.valueIds,
StartIndex: 0, StartIndex: endIndex,
}, },
baseURL: useRuntimeConfig().public.baseUrl, baseURL: useRuntimeConfig().public.baseUrl,
}).then((res) => {
console.log(res)
const data = res as Data;
if (data.done) {
isLoading.value = false
done = true
return
}
endIndex = data.endIndex
//data
const currentOption = chartRet.value.getOption();
currentOption.series.map((series: any, index: number) => {
series.data = [...data.data[props.valueIds[index]], ...series.data]
})
currentOption.xAxis[0].data = [...data.times, ...currentOption.xAxis[0].data]
//start 5
currentOption.dataZoom[0].start = 55
chartRet.value.setOption(currentOption)
setTimeout(() => {
isLoading.value = false
}, 1000)
}) })
}, 2000) }
}, 1000)
})
// let endIndex = dataStore.dataHistory.times ? dataStore.dataHistory.times.length : 0
// let startTemp = 0;
// let done = false;
// type Data = {
// times: string[]; //
// data: { [key: string]: string[] }; //
// endIndex: number; //
// done: boolean; //
// };
// const zoom = _.throttle((e: any) => {
// console.log(e)
// let start = e.start ?? e.batch[0].start
// if (done) return
// if (start <= 50 && start !== startTemp) {
// startTemp = e.start
// isLoading.value = true
// setTimeout(() => {
// isLoading.value = false
// }, 5000)
// $fetch('/Api/Server/GetServerHistoryDate', {
// method: 'GET',
// headers: {
// 'Authorization': 'Bearer ' + useCookie('token').value
// },
// params: {
// ServerId: mainLayoutStore.SelectServer.value,
// DataTypes: props.valueIds,
// StartIndex: endIndex,
// },
// baseURL: useRuntimeConfig().public.baseUrl,
// }).then((res) => {
// console.log(res)
// const data = res as Data;
// if (data.done) {
// isLoading.value = false
// done = true
// return
// }
// endIndex = data.endIndex
// //data
// const currentOption = chartRet.value.getOption();
// currentOption.series.map((series: any, index: number) => {
// series.data = [...data.data[props.valueIds[index]], ...series.data]
// })
// currentOption.xAxis[0].data = [...data.times, ...currentOption.xAxis[0].data]
// //start 5
// currentOption.dataZoom[0].start = 55
// chartRet.value.setOption(currentOption)
// setTimeout(() => {
// isLoading.value = false
// }, 1000)
// })
// }
// }, 1000)
</script> </script>
<template> <template>
<v-chart ref="chartRef" :loading="isLoading" :manual-update="true" :option="option" <v-chart ref="chartRet" :loading="isLoading" :manual-update="true" :option="option"
autoresize autoresize
:theme="$colorMode.value" :theme="$colorMode.value"
class="chart" class="chart"
/> @datazoom="zoom"/>
</template> </template>
<style> <style>

View File

@ -60,6 +60,15 @@ const option = computed(() => {
type: 'value', type: 'value',
} }
], ],
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
saveAsImage: {}
}
},
dataZoom: [ dataZoom: [
{ {
type: 'inside', type: 'inside',
@ -79,8 +88,6 @@ const option = computed(() => {
], ],
legend: { legend: {
data: props.valueNames, data: props.valueNames,
right:"5",
top:"1",
textStyle: { textStyle: {
fontSize: 16, fontSize: 16,
}, },
@ -94,15 +101,6 @@ const option = computed(() => {
emphasis: { emphasis: {
focus: 'series' focus: 'series'
}, },
markPoint: {
data: [
{ type: 'max', name: 'Max' },
{ type: 'min', name: 'Min' }
]
},
markLine: {
data: [{ type: 'average', name: 'Avg' }]
},
data: [], data: [],
} }
}) ?? [] }) ?? []

View File

@ -108,9 +108,6 @@ watchDebounced(
<n-modal <n-modal
v-model:show="visible" v-model:show="visible"
style="width: auto;" style="width: auto;"
preset="card"
title="添加图表"
> >
<SettingCard /> <SettingCard />
</n-modal> </n-modal>

View File

@ -1,134 +0,0 @@
<script lang="ts" setup>
import type {PropType} from "vue";
const props = defineProps({
label: String,
cardType: String as PropType<'Error' | 'Informative' | 'Success' | 'Warning'>,
time: Number,
text: String,
})
const notificationBoxClass = computed(() => ({
'Notification-Box': true,
'Notification-Box-Error': props.cardType === 'Error',
'Notification-Box-Informative': props.cardType === 'Informative',
'Notification-Box-Success': props.cardType === 'Success',
'Notification-Box-Warning': props.cardType === 'Warning'
}));
</script>
<template>
<div :class="notificationBoxClass">
<div class="Icon">
<NuxtImg v-if="cardType==='Success'" src="/Success.svg" width="24"></NuxtImg>
<NuxtImg v-if="cardType==='Informative'" src="/Informative.svg" width="24"></NuxtImg>
<NuxtImg v-if="cardType==='Warning'" src="/Warning.svg" width="24"></NuxtImg>
<NuxtImg v-if="cardType==='Error'" src="/Error.svg" width="24"></NuxtImg>
</div>
<div class="Text">
<h3>{{ label }}</h3>
<p>{{ text }}</p>
</div>
<Icon :stroke-width="1.2" name="X"></Icon>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.Notification-Box {
padding: $padding;
display: grid;
grid-template-columns: 24px 1fr 18px;
gap: $gap*2;
grid-template-rows: 1fr;
border-radius: 12px;
> svg {
stroke: #979FA9;
.dark-mode & {
stroke: #FFF;
}
}
}
.Icon {
.dark-mode & {
filter: grayscale(30%) brightness(100%);
}
}
.Text {
display: flex;
flex-direction: column;
gap: $gap*.5;
.dark-mode & {
> h3, p {
color: #FFF;
}
}
> h3 {
color: #27303A;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 140%; /* 19.6px */
text-transform: capitalize;
}
> p {
color: #2F3F53;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 160%; /* 19.2px */
}
}
.Notification-Box-Success {
border: 1.3px solid #48C1B5;
background: #F6FFF9;
.dark-mode & {
border: 1.3px solid #43D590;
background: unset;
}
}
.Notification-Box-Informative {
border: 1.3px solid #9DC0EE;
background: #F5F9FF;
.dark-mode & {
border: 1.3px solid #7BCFED;
background: unset;
}
}
.Notification-Box-Warning {
border: 1.3px solid #F7D9A4;
background: #FFF8EC;
.dark-mode & {
border: 1.3px solid #FFDF8D;
background: unset;
}
}
.Notification-Box-Error {
border: 1.3px solid #F4B0A1;
background: #FFF5F3;
.dark-mode & {
border: 1.3px solid #F0863A;
background: unset;
}
}
</style>

View File

@ -10,14 +10,6 @@ import _ from "lodash";
var term: Terminal; var term: Terminal;
const terminal = ref<HTMLDivElement|null>(null); const terminal = ref<HTMLDivElement|null>(null);
const props=defineProps(
{
isPage:{
type:Boolean,
default:false
}
}
)
const resizeObserver =_.debounce(()=>{ const resizeObserver =_.debounce(()=>{
if(terminal.value===null) return; if(terminal.value===null) return;
const cols = Math.floor(terminal.value.clientWidth / 9); const cols = Math.floor(terminal.value.clientWidth / 9);
@ -32,11 +24,8 @@ onMounted(()=>{
}}); }});
term.open(document.getElementById("terminal") as HTMLElement); term.open(document.getElementById("terminal") as HTMLElement);
if(terminal.value===null) return; if(terminal.value===null) return;
let cols = Math.floor(terminal.value.clientWidth / 9); const cols = Math.floor(terminal.value.clientWidth / 9);
let rows = Math.floor(terminal.value.clientHeight / 17); const rows = Math.floor(terminal.value.clientHeight / 17);
if (props.isPage) {
cols-=1
}
term.resize(cols-2, rows-1) term.resize(cols-2, rows-1)
connection.invoke("ResizeTerminal", rows-1, cols-2) connection.invoke("ResizeTerminal", rows-1, cols-2)
term.focus(); term.focus();
@ -75,9 +64,9 @@ onMounted(async () => {
.then(() => { .then(() => {
console.log("Terminal created") console.log("Terminal created")
}) })
// setTimeout(()=>{ setTimeout(()=>{
// connection.invoke("SendMessage","neofetch\n") connection.invoke("SendMessage","neofetch\n")
// },200) },200)
}) })
const resize=()=>{ const resize=()=>{
@ -91,9 +80,7 @@ const resize=()=>{
let width = x - terminal.value.getBoundingClientRect().left; let width = x - terminal.value.getBoundingClientRect().left;
let height = y - terminal.value.getBoundingClientRect().top; let height = y - terminal.value.getBoundingClientRect().top;
// terminal // terminal
if(!props.isPage){ terminal.value.style.width = `${width}px`;
terminal.value.style.width = `${width}px`;
}
terminal.value.style.height = `${height}px`; terminal.value.style.height = `${height}px`;
} }
document.addEventListener('mousemove', onMouseMove, { passive: true }); document.addEventListener('mousemove', onMouseMove, { passive: true });
@ -104,9 +91,7 @@ const resize=()=>{
width = Math.round(width / 9) * 9+20; width = Math.round(width / 9) * 9+20;
let height = terminal.value.getBoundingClientRect().height; let height = terminal.value.getBoundingClientRect().height;
height = Math.round(height / 17) * 17+20; height = Math.round(height / 17) * 17+20;
if(!props.isPage){ terminal.value.style.width = `${width}px`;
terminal.value.style.width = `${width}px`;
}
terminal.value.style.height = `${height}px`; terminal.value.style.height = `${height}px`;
resizeObserver() resizeObserver()
// //
@ -122,29 +107,10 @@ const resize=()=>{
</script> </script>
<template> <template>
<div :class="{'terminal-box':true,'terminal-box-page':isPage}" ref="terminal"> <div class="terminal-box" ref="terminal">
<div id="terminal" @resize="resizeObserver" > <div id="terminal" @resize="resizeObserver" >
</div> </div>
<ArrowDownRight class="arrow" @mousedown="resize" /> <ArrowDownRight class="arrow" @mousedown="resize" />
<svg
class="bg2"
xmlns='http://www.w3.org/2000/svg'
style="background: black;filter:grayscale(1);"
>
<filter id='noiseFilter'>
<feTurbulence
type='fractalNoise'
baseFrequency='0.65'
numOctaves='3'
stitchTiles='stitch' />
</filter>
<rect
width='100%'
height='100%'
filter='url(#noiseFilter)' />
</svg>
</div> </div>
@ -159,29 +125,11 @@ const resize=()=>{
height: 500px; height: 500px;
max-width: 80vw; max-width: 80vw;
max-height: 80vh; max-height: 80vh;
min-width: 400px; border-radius: $radius;
min-height: 300px; background: rgba(0,0,0,.5);
border-radius: $radius*2; backdrop-filter: blur(20px)
backdrop-filter: blur(20px);
border: 2px solid #fff;
will-change: contents;
}
.terminal-box-page{
border: unset;
padding: unset;
width: 100%;
max-width: unset;
}
.bg2{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
pointer-events: none;
opacity: 0.1;
} }
#terminal { #terminal {
cursor: pointer; cursor: pointer;
&::-webkit-scrollbar{ &::-webkit-scrollbar{

View File

@ -8,7 +8,11 @@ const userInfo = toRef(MainLayoutStore.UserInfo)
<template> <template>
<div class="User-Mini-Box"> <div class="User-Mini-Box">
<NuxtImg :src="userInfo.avatar" alt="Avatar" height="45" width="45"/> <n-avatar
round
:size="45"
:src="userInfo.avatar"
/>
</div> </div>
</template> </template>

View File

@ -25,6 +25,7 @@ onMounted(() => {
width: 100%; width: 100%;
padding: 32px; padding: 32px;
gap: 32px; gap: 32px;
background: unset;
display: grid; display: grid;
grid-template-columns: 1fr minmax(800px,1fr); grid-template-columns: 1fr minmax(800px,1fr);
grid-template-rows: 1fr; grid-template-rows: 1fr;

View File

@ -7,7 +7,8 @@ import type {UserInfoType} from "~/types/UserType";
import type {HttpType} from "~/types/baseType"; import type {HttpType} from "~/types/baseType";
import {useSessionSignalRStore} from "~/strores/HubStore"; import {useSessionSignalRStore} from "~/strores/HubStore";
import {POSITION, useToast} from "vue-toastification"; import {POSITION, useToast} from "vue-toastification";
import { useDataStore} from "~/strores/DataStore"; import {type dataHistoryType, useDataStore} from "~/strores/DataStore";
import VueDragResize from 'vue-drag-resize'
const audio = ref<any>(null); const audio = ref<any>(null);
const audio1 = ref<any>(null); const audio1 = ref<any>(null);
@ -15,6 +16,7 @@ const audio2 = ref<any>(null);
const toast = useToast() const toast = useToast()
const visible = ref<boolean>(false) const visible = ref<boolean>(false)
const DataStore = useDataStore() const DataStore = useDataStore()
const {$gsap} = useNuxtApp()
const mainRef = ref<HTMLElement>() const mainRef = ref<HTMLElement>()
const mainLayoutStore = useMainLayoutStore() const mainLayoutStore = useMainLayoutStore()
const ServerList = ref<{ label: string, value: string }[]>([]) const ServerList = ref<{ label: string, value: string }[]>([])
@ -130,9 +132,40 @@ onMounted(() => {
}) })
}) })
onUnmounted(() => { onUnmounted(() => {
//
signalR.connection?.stop() signalR.connection?.stop()
}) })
const getHistoryData = async () => {
$fetch('/Api/Server/GetServerHistoryDate', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + useCookie('token').value
},
params: {
ServerId: mainLayoutStore.SelectServer.value,
},
baseURL: useRuntimeConfig().public.baseUrl,
}).then((res) => {
const data = res as dataHistoryType
DataStore.dataHistory = data
const datas = data.data
//
for (const key in datas) {
if (Object.prototype.hasOwnProperty.call(datas, key)) {
const element = datas[key];
//
DataStore.data[key] = element[element.length - 1];
}
}
DataStore.startTimer()
})
}
onMounted(async () => {
await getHistoryData()
})
watch(() => mainLayoutStore.SelectServer.value, async () => {
await getHistoryData()
})
let isShift = false let isShift = false
onKeyStroke('Shift', (e) => { onKeyStroke('Shift', (e) => {
e.preventDefault() e.preventDefault()
@ -166,6 +199,7 @@ onKeyStroke('Shift', (e) => {
<SideBar/> <SideBar/>
<TitleBar/> <TitleBar/>
<n-modal v-model:show.lazy="visible" auto-focus > <n-modal v-model:show.lazy="visible" auto-focus >
<Term/> <Term/>
</n-modal> </n-modal>
<div class="main-box"> <div class="main-box">

View File

@ -1,24 +1,22 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
export default defineNuxtConfig({ export default defineNuxtConfig({
devtools: {enabled: true}, devtools: {enabled: true},
ssr: false, ssr: false,
modules: [ modules: [
'@nuxtjs/color-mode', '@nuxtjs/color-mode',
'@hypernym/nuxt-gsap', '@hypernym/nuxt-gsap',
"@nuxt/image", "@nuxtjs/google-fonts",
"@nuxtjs/google-fonts", 'nuxt-lucide-icons',
'nuxt-lucide-icons', '@pinia/nuxt',
'@pinia/nuxt', '@pinia-plugin-persistedstate/nuxt',
'@pinia-plugin-persistedstate/nuxt', "@vueuse/nuxt",
"@vite-pwa/nuxt", "nuxtjs-naive-ui"
"@vueuse/nuxt", ],
"nuxtjs-naive-ui"
],
vite: { vite: {
plugins: [ plugins: [
AutoImport({ AutoImport({
@ -38,66 +36,65 @@ export default defineNuxtConfig({
}) })
] ]
}, },
pwa: { // pwa: {
manifest: { // manifest: {
name: "pwa nuxt 3", // name: "pwa nuxt 3",
short_name: "pwa nuxt", // short_name: "pwa nuxt",
theme_color: '#FFFFFF', // theme_color: '#FFFFFF',
description: "Arman Abi r.man.abi@gmail.com", // description: "Arman Abi r.man.abi@gmail.com",
icons: [{ // icons: [{
src: '/pwa-192x192.png', sizes: "192x192", type: "image/png" // src: '/pwa-192x192.png', sizes: "192x192", type: "image/png"
},] // },]
//
//
// }, workbox: {
// navigateFallback: "/SignIn",
//
// }, devOptions: {
// enabled: true, type: "module"
// }
//
// },
gsap: {
autoImport: true,
extraPlugins: {
text: true
}
},
}, workbox: { css: ['assets/min.scss', 'vue-toastification/dist/index.css'],
navigateFallback: "/SignIn",
}, devOptions: { devServer: {
enabled: true, type: "module" port: 3001, host: '0.0.0.0',
} // https: {
// key: "./localhost+3-key.pem",
// cert: "./localhost+3.pem",
// }
},
}, plugins: [
{src: '~/plugins/vue-toast.ts'},
{src: '~/plugins/apexcharts.ts'},
],
gsap: { runtimeConfig: {
autoImport: true, baseUrl: '', public: {
extraPlugins: { apiBase: '/Api', baseUrl: import.meta.env.NUXT_API_URL,
text: true }
} },
},
css: ['assets/min.scss', 'vue-toastification/dist/index.css'], googleFonts: {
families: {
Roboto: true, 'Josefin+Sans': true, Lato: [100, 300], Raleway: {
wght: [100, 400], ital: [100]
}, Inter: '200..700', 'Crimson Pro': {
wght: '200..900', ital: '200..700',
}, 'Noto Sans SC': {
wght: '200..900', ital: '200..700'
}
}
},
devServer: { compatibilityDate: '2024-07-21',
port: 3001,
// host: '0.0.0.0',
// https: {
// key: "./localhost+3-key.pem",
// cert: "./localhost+3.pem",
// }
},
plugins: [
{src: '~/plugins/vue-toast.ts'},
{src: '~/plugins/apexcharts.ts'},
],
runtimeConfig: {
baseUrl: '', public: {
apiBase: '/Api', baseUrl: process.env.NUXT_API_URL
}
},
googleFonts: {
families: {
Roboto: true, 'Josefin+Sans': true, Lato: [100, 300], Raleway: {
wght: [100, 400], ital: [100]
}, Inter: '200..700', 'Crimson Pro': {
wght: '200..900', ital: '200..700',
}, 'Noto Sans SC': {
wght: '200..900', ital: '200..700'
}
}
},
compatibilityDate: '2024-07-21',
}) })

View File

@ -4,6 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"build:yuan": "nuxt build --dotenv .env.yuan",
"dev": "nuxt dev --dotenv .env.development", "dev": "nuxt dev --dotenv .env.development",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
@ -11,31 +12,32 @@
"postUpdate": "patch-package", "postUpdate": "patch-package",
"start": "nuxt start" "start": "nuxt start"
}, },
"resolutions": {
"rollup": "npm:@rollup/wasm-node"
},
"dependencies": { "dependencies": {
"@nuxt/image": "^1.7.0",
"@nuxtjs/google-fonts": "^3.2.0", "@nuxtjs/google-fonts": "^3.2.0",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@vite-pwa/nuxt": "^0.8.0", "@vite-pwa/nuxt": "^0.8.0",
"@vueuse/components": "^10.10.1", "@vueuse/components": "^10.10.1",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.12",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"grid-layout-plus": "^1.0.5", "grid-layout-plus": "^1.0.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"md-editor-v3": "^4.17.0", "md-editor-v3": "^4.17.0",
"naive-ui": "^2.39.0", "naive-ui": "^2.39.0",
"nuxt": "^3.11.2", "nuxt": "^3.11.2",
"nuxt-primevue": "^3.0.0",
"nuxtjs-naive-ui": "^1.0.2", "nuxtjs-naive-ui": "^1.0.2",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"primeicons": "^7.0.0", "pinia": "^2.2.0",
"unplugin-auto-import": "^0.18.0", "unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.3", "unplugin-vue-components": "^0.27.3",
"uuid": "^9.0.1", "uuid": "^10.0.0",
"vue": "^3.4.27", "vue": "^3.4.35",
"vue-drag-resize": "^1.5.4", "vue-drag-resize": "^1.5.4",
"vue-echarts": "^6.7.3", "vue-echarts": "^7.0.0-beta.0",
"vue-router": "^4.3.2", "vue-router": "^4.4.2",
"vue-toastification": "^2.0.0-rc.5", "vue-toastification": "^2.0.0-rc.5",
"vue3-apexcharts": "^1.5.3", "vue3-apexcharts": "^1.5.3",
"vue3-auth-code-input": "^1.0.10", "vue3-auth-code-input": "^1.0.10",
@ -47,15 +49,13 @@
"@microsoft/signalr": "^8.0.0", "@microsoft/signalr": "^8.0.0",
"@nuxtjs/color-mode": "^3.4.1", "@nuxtjs/color-mode": "^3.4.1",
"@pinia-plugin-persistedstate/nuxt": "^1.2.0", "@pinia-plugin-persistedstate/nuxt": "^1.2.0",
"@pinia/nuxt": "^0.5.1", "@pinia/nuxt": "^0.5.2",
"@types/uuid": "^9.0.8", "@types/uuid": "^10.0.0",
"@vite-pwa/assets-generator": "^0.2.4",
"@vueuse/core": "^10.11.0", "@vueuse/core": "^10.11.0",
"@vueuse/nuxt": "^10.10.0", "@vueuse/nuxt": "^10.10.0",
"nuxt-gsap-module": "^2.0.0", "nuxt-gsap-module": "^2.0.0",
"nuxt-lucide-icons": "^1.0.4", "nuxt-lucide-icons": "^1.0.4",
"sass": "^1.77.4", "sass": "^1.77.4",
"vue3-draggable-grid": "^0.0.6",
"vue3-puzzle-vcode": "^1.1.7" "vue3-puzzle-vcode": "^1.1.7"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"

View File

@ -107,11 +107,11 @@ const onSuccess = () => {
</div> </div>
<div class="social-button"> <div class="social-button">
<button> <button>
<NuxtImg src="/Google.svg"></NuxtImg> <img src="/Google.svg" alt=""/>
<p>使用 Google 帐号登录</p> <p>使用 Google 帐号登录</p>
</button> </button>
<button> <button>
<NuxtImg src="/Facebook.svg"></NuxtImg> <img src="/Facebook.svg" alt=""/>
<p>使用 Facebook 登录</p></button> <p>使用 Facebook 登录</p></button>
</div> </div>
</div> </div>

View File

@ -8,7 +8,7 @@ definePageMeta({
<template> <template>
<div class="terminal-layout"> <div class="terminal-layout">
<Term is-page/> <Term/>
</div> </div>
</template> </template>
@ -20,8 +20,7 @@ definePageMeta({
border-radius: $radius; border-radius: $radius;
border: $border; border: $border;
background: #000; background: #000;
padding: $padding; padding: $padding*2;
overflow: hidden;
} }

View File

@ -12,14 +12,11 @@ import {
CalendarComponent, CalendarComponent,
AriaComponent, AriaComponent,
BrushComponent, BrushComponent,
MarkLineComponent,
MarkPointComponent,
TitleComponent, TitleComponent,
LegendComponent, LegendComponent,
DataZoomComponent DataZoomComponent
} from 'echarts/components'; } from 'echarts/components';
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
use([CanvasRenderer, BarChart, GaugeChart, LinesChart, LineChart, PieChart, GridComponent, TooltipComponent, ToolboxComponent, TimelineComponent, TitleComponent, LegendComponent, DataZoomComponent,MarkLineComponent, use([CanvasRenderer, BarChart, GaugeChart, LinesChart, LineChart, PieChart, GridComponent, TooltipComponent, ToolboxComponent, TimelineComponent, TitleComponent, LegendComponent, DataZoomComponent]);
MarkPointComponent]);
}); });

View File

@ -5,11 +5,42 @@ import dayjs from "dayjs";
export const useDataStore = defineStore('DataStore', { export const useDataStore = defineStore('DataStore', {
state: () => ({ state: () => ({
data: <dataType>{}, data: <dataType>{},
dataHistory: <dataHistoryType>{...defaultDataHistory},
timer: null as any,
}), }),
actions: { actions: {
setData(type: string, data: any) { setData(type: string, data: any) {
//不存在则添加,存在则更新
this.data[type] = data this.data[type] = data
}, },
startTimer() {
if (this.timer) return; // 防止重复启动定时器
this.timer = setInterval(() => {
// 每秒将 data 中的数据添加到 dataHistory 中
this.addDataToHistory();
}, 5000);
},
stopTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
addDataToHistory() {
const currentTime = dayjs().format('MM-DD HH:mm:ss');
for (const key in this.data) {
const value = this.data[key];
if (!this.dataHistory.data[key]) {
this.dataHistory.data[key] = [];
}
this.dataHistory.data[key].push(value)
}
this.dataHistory.times.push(currentTime);
}
},
persist: {
storage: persistedState.sessionStorage,
}, },
}) })
@ -17,3 +48,15 @@ export const useDataStore = defineStore('DataStore', {
export type dataType = { export type dataType = {
[key: string]: string; [key: string]: string;
}; };
export type dataHistoryType = {
times: string[],
data: dataHistoryDataType,
}
type dataHistoryDataType = {
[key: string]: string[]
}
const defaultDataHistory: dataHistoryType = {
times: [],
data: {}
}

View File

@ -44,6 +44,7 @@ export function useBeep() {
export function toggleDark(event: MouseEvent) { export function toggleDark(event: MouseEvent) {
const isAppearanceTransition = document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches const isAppearanceTransition = document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!isAppearanceTransition) { if (!isAppearanceTransition) {
useColorMode().preference = useColorMode().value == 'dark' ? 'light' : 'dark' useColorMode().preference = useColorMode().value == 'dark' ? 'light' : 'dark'
return return

File diff suppressed because it is too large Load Diff