From 79c9e36d89760462eef9a0e06d1210645968ee88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E5=85=86=E5=B0=89?= <228127304@qq.com> Date: Wed, 26 Aug 2026 17:48:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=20MiGu=20=E4=BE=A7=E8=90=BD=E5=9C=B0?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=AE=9A=E6=97=B6=E6=B8=85=E7=90=86=E4=B8=8E?= =?UTF-8?q?=E7=A3=81=E7=9B=98=E7=A9=BA=E9=97=B4=E5=91=8A=E8=AD=A6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 系统配置可开关清理策略;日志管理页支持查看/保存清理选项。 Co-authored-by: Cursor --- MiGu.Server/Configs/ConfigStore.cs | 54 ++- MiGu.Server/Configs/LogCleanupOptions.cs | 39 ++ MiGu.Server/Configs/SystemConfig.cs | 12 +- MiGu.Server/Controllers/ConfigController.cs | 7 +- MiGu.Server/Controllers/LogsController.cs | 72 +++- MiGu.Server/Logs/LogCleanupService.cs | 270 +++++++++++++ MiGu.Server/data/config-system.json | 6 +- .../apps/simple-platform-vue/src/api/logs.ts | 71 +++- .../simple-platform-vue/src/types/config.ts | 15 - .../src/views/admin/LogManagementView.vue | 74 +++- .../src/views/admin/config/OpsConfigView.vue | 18 +- .../views/admin/config/SystemConfigView.vue | 368 +++++++++++++----- 12 files changed, 856 insertions(+), 150 deletions(-) create mode 100644 MiGu.Server/Configs/LogCleanupOptions.cs create mode 100644 MiGu.Server/Logs/LogCleanupService.cs diff --git a/MiGu.Server/Configs/ConfigStore.cs b/MiGu.Server/Configs/ConfigStore.cs index da99bdf..f43a4e4 100644 --- a/MiGu.Server/Configs/ConfigStore.cs +++ b/MiGu.Server/Configs/ConfigStore.cs @@ -7,7 +7,7 @@ namespace MiGu.Server.Configs; /// /// 配置中心存储(内存 + JSON 文件持久化占位)。 /// 前 14 个 section 对应 ARCHITECTURE.md §9 的 13+1 维度;外加 deployment —— 登录后「配置向导」的部署画像。 -/// 真实落地时由 SimpleShared.Persistence 接入 EF Core,并配合 YARP 下发至 SimpleLite。 +/// 真实落地时由 SimpleShared.Persistence 接入 EF Core,并配合 YARP 下发至 Simple3。 /// public sealed class ConfigStore { @@ -99,6 +99,58 @@ public sealed class ConfigStore return Put("deployment", el); } + public LogCleanupOptions GetLogCleanup() + { + var env = Get("system"); + return env.Payload switch + { + SystemConfig sc => (sc.LogCleanup ?? new LogCleanupOptions()).Clamp(), + JsonElement el => ParseLogCleanup(el), + _ => LogCleanupOptions.CreateDefaults() + }; + } + + public Envelope PutLogCleanup(LogCleanupOptions options) + { + var opt = (options ?? new LogCleanupOptions()).Clamp(); + Dictionary dict; + try + { + var current = Get("system").Payload; + var el = current is JsonElement je + ? je + : JsonSerializer.SerializeToElement(current, _jsonOpts); + dict = el.ValueKind == JsonValueKind.Object + ? JsonSerializer.Deserialize>(el.GetRawText()) ?? new() + : new Dictionary(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "读取 system 配置失败,按空对象合并 logCleanup"); + dict = new Dictionary(); + } + + dict["logCleanup"] = JsonSerializer.SerializeToElement(opt, _jsonOpts); + var merged = JsonSerializer.SerializeToElement(dict, _jsonOpts); + return Put("system", merged); + } + + private LogCleanupOptions ParseLogCleanup(JsonElement el) + { + try + { + if (el.ValueKind == JsonValueKind.Object && el.TryGetProperty("logCleanup", out var nested)) + return (nested.Deserialize(_jsonOpts) ?? new LogCleanupOptions()).Clamp(); + var sc = el.Deserialize(_jsonOpts); + return (sc?.LogCleanup ?? new LogCleanupOptions()).Clamp(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "反序列化 logCleanup 失败,回退默认值"); + return LogCleanupOptions.CreateDefaults(); + } + } + private DeploymentProfile SafeDeserializeDeployment(JsonElement el) { try { return el.Deserialize(_jsonOpts) ?? DeploymentProfile.Default(); } diff --git a/MiGu.Server/Configs/LogCleanupOptions.cs b/MiGu.Server/Configs/LogCleanupOptions.cs new file mode 100644 index 0000000..71a26e7 --- /dev/null +++ b/MiGu.Server/Configs/LogCleanupOptions.cs @@ -0,0 +1,39 @@ +namespace MiGu.Server.Configs; + +/// +/// 日志清理与磁盘告警(system.logCleanup)。字段对齐 Simple3 LogCleanupOptions。 +/// +public sealed class LogCleanupOptions +{ + /// 是否启用后台自动清理。关闭后仅「立即清理」可手动触发。默认 true。 + public bool Enabled { get; set; } = true; + + /// 保留天数:删除 log/ 下 LastWriteTime 早于「现在 − N 天」的 *.log。默认 30,最小 1。 + public int RetentionDays { get; set; } = 30; + + /// 后台清理间隔(小时)。默认 24;<= 0 归一为 1。 + public int CheckIntervalHours { get; set; } = 24; + + /// 启动时先执行一次清理。默认 true。 + public bool RunOnStartup { get; set; } = true; + + /// 是否启用磁盘剩余空间不足告警。默认 true。 + public bool DiskAlertEnabled { get; set; } = true; + + /// 日志所在盘剩余低于该值(GB)时告警。默认 5。 + public double DiskFreeAlertGB { get; set; } = 5; + + /// 磁盘检测间隔(分钟)。默认 30;<= 0 归一为 30。 + public int DiskCheckIntervalMinutes { get; set; } = 30; + + public static LogCleanupOptions CreateDefaults() => new(); + + public LogCleanupOptions Clamp() + { + RetentionDays = Math.Clamp(RetentionDays, 1, 3650); + CheckIntervalHours = Math.Max(1, CheckIntervalHours); + if (DiskFreeAlertGB <= 0) DiskFreeAlertGB = 5; + DiskCheckIntervalMinutes = DiskCheckIntervalMinutes <= 0 ? 30 : DiskCheckIntervalMinutes; + return this; + } +} diff --git a/MiGu.Server/Configs/SystemConfig.cs b/MiGu.Server/Configs/SystemConfig.cs index c6828cc..cc93c78 100644 --- a/MiGu.Server/Configs/SystemConfig.cs +++ b/MiGu.Server/Configs/SystemConfig.cs @@ -1,12 +1,8 @@ namespace MiGu.Server.Configs; -public record LogPolicy(string Level, int RollDays, int MaxSizeMB); -public record SecurityPolicy(int JwtExpireMin, bool EnableSwagger, List CorsWhitelist); - -public record SystemConfig(int DispatchLoopHz, LogPolicy Log, SecurityPolicy Security) +public record SystemConfig { - public static SystemConfig Default() => new( - DispatchLoopHz: 50, - Log: new LogPolicy("info", 7, 256), - Security: new SecurityPolicy(1440, false, new List { "http://localhost:5173" })); + public LogCleanupOptions LogCleanup { get; init; } = new(); + + public static SystemConfig Default() => new(); } diff --git a/MiGu.Server/Controllers/ConfigController.cs b/MiGu.Server/Controllers/ConfigController.cs index ca5010b..91ff780 100644 --- a/MiGu.Server/Controllers/ConfigController.cs +++ b/MiGu.Server/Controllers/ConfigController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MiGu.Server.Configs; +using MiGu.Server.Logs; namespace MiGu.Server.Controllers; @@ -13,10 +14,12 @@ namespace MiGu.Server.Controllers; public class ConfigController : ControllerBase { private readonly ConfigStore _store; + private readonly LogCleanupService _cleanup; - public ConfigController(ConfigStore store) + public ConfigController(ConfigStore store, LogCleanupService cleanup) { _store = store; + _cleanup = cleanup; } [HttpGet] @@ -66,6 +69,8 @@ public class ConfigController : ControllerBase } var env = _store.Put(section, payload); + if (string.Equals(section, "system", StringComparison.OrdinalIgnoreCase)) + _cleanup.ApplySchedule(); return Ok(new { section = env.Section, diff --git a/MiGu.Server/Controllers/LogsController.cs b/MiGu.Server/Controllers/LogsController.cs index 4302380..de1e5f0 100644 --- a/MiGu.Server/Controllers/LogsController.cs +++ b/MiGu.Server/Controllers/LogsController.cs @@ -2,18 +2,20 @@ using System.Globalization; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using MiGu.Server.Configs; using MiGu.Server.Launcher; +using MiGu.Server.Logs; namespace MiGu.Server.Controllers; /// -/// 平台「日志管理」后端:把 SimpleLite 内核 SimpleCore.Library.Diagnosis 的落盘日志 +/// 平台「日志管理」后端:把 Simple3 内核 SimpleCore.Library.Diagnosis 的落盘日志 /// (Diagnosis.Post / Diagnosis.Log 写入的 log/**/*.log,俗称 DLog)暴露给 -/// platform-vue 的配置中心「日志管理」页查看。对齐 SimpleLite 桌面端 SimpleLite/UI/LogViewer.cs +/// platform-vue 的配置中心「日志管理」页查看。对齐 Simple3 桌面端 Simple3/UI/LogViewer.cs /// 的两区设计(诊断条目表 + 落盘文件表),并在 Web 端额外提供「按标签合订」视图。 /// -/// 数据来源:日志是 SimpleLite 进程在其工作目录写出的历史文件,不依赖 SimpleLite 是否在运行, -/// MiGu.Server 通过 定位工作目录后直接读 +/// 数据来源:日志是 Simple3 进程在其工作目录写出的历史文件,不依赖 Simple3 是否在运行, +/// MiGu.Server 通过 定位工作目录后直接读 /// {工作目录}/log/。可用 appsettings Logs:Root 显式覆盖日志根目录。 /// /// 落盘行格式(见 Diagnosis.Log):[{prefix}yyyy/MM/dd-HH:mm:ss.fff] >{tag}: {content}, @@ -59,15 +61,21 @@ public sealed class LogsController : ControllerBase private static readonly Regex NumericFieldRegex = new(@"(?[A-Za-z_\u4e00-\u9fff][\w\u4e00-\u9fff\.]*)\s*[=:]\s*(?-?\d+(?:\.\d+)?)", RegexOptions.Compiled); - private readonly SimpleLiteLauncher _launcher; + private readonly Simple3Launcher _launcher; private readonly IConfiguration _config; private readonly ILogger _log; + private readonly LogCleanupService _cleanup; - public LogsController(SimpleLiteLauncher launcher, IConfiguration config, ILogger log) + public LogsController( + Simple3Launcher launcher, + IConfiguration config, + ILogger log, + LogCleanupService cleanup) { _launcher = launcher; _config = config; _log = log; + _cleanup = cleanup; } // ─────────────────────────────────────────────────────────── 概览 ── @@ -78,14 +86,15 @@ public sealed class LogsController : ControllerBase { var (root, workdir, error) = ResolveLogRoot(); if (root == null) - return Ok(new { exists = false, root = (string?)null, workingDirectory = workdir, message = error }); + return Ok(new { exists = false, root = (string?)null, workingDirectory = workdir, message = error, disk = DiskDto() }); if (!Directory.Exists(root)) return Ok(new { exists = false, root, workingDirectory = workdir, totalFiles = 0, totalBytes = 0L, days = Array.Empty(), - message = "日志目录尚未生成(SimpleLite 产生落盘日志后会自动创建 log 目录)。" + message = "日志目录尚未生成(Simple3 产生落盘日志后会自动创建 log 目录)。", + disk = DiskDto() }); var files = EnumerateLogFiles(root); @@ -101,7 +110,32 @@ public sealed class LogsController : ControllerBase totalFiles = files.Count, totalBytes = files.Sum(f => f.Bytes), latestFileTime = files.Count > 0 ? files.Max(f => f.Mtime) : (DateTime?)null, - days + days, + disk = DiskDto() + }); + } + + /// 日志清理配置(对齐 Simple3 logCleanup)。 + [HttpGet("cleanup-config")] + public IActionResult GetCleanupConfig() => Ok(_cleanup.GetOptions()); + + /// 保存日志清理配置并立即重排后台定时器。 + [HttpPut("cleanup-config")] + public IActionResult PutCleanupConfig([FromBody] LogCleanupOptions body) + => Ok(_cleanup.SaveOptions(body ?? new LogCleanupOptions())); + + /// 按当前保留天数立即清理一次过期 *.log。 + [HttpPost("cleanup-now")] + public IActionResult CleanupNow() + { + var r = _cleanup.CleanNow(); + if (r.Error != null) + return Problem(r.Error, statusCode: 500); + return Ok(new + { + deletedFiles = r.DeletedFiles, + freedBytes = r.FreedBytes, + freedMB = Math.Round(r.FreedMB, 1) }); } @@ -151,7 +185,7 @@ public sealed class LogsController : ControllerBase root = normRoot, exists = false, path = "", parent = (string?)null, dirCount = 0, fileCount = 0, dirs = Array.Empty(), files = Array.Empty(), - message = "日志目录尚未生成(SimpleLite 产生落盘日志后会自动创建 log 目录)。" + message = "日志目录尚未生成(Simple3 产生落盘日志后会自动创建 log 目录)。" }); var target = SafeResolveDir(normRoot, path); @@ -592,9 +626,23 @@ public sealed class LogsController : ControllerBase return PhysicalFile(full, "application/octet-stream", downloadName); } + private object DiskDto() + { + var d = _cleanup.GetDiskStatus(); + return new + { + known = d.Known, + drive = d.Drive, + freeGB = Math.Round(d.FreeGB, 1), + alertEnabled = d.AlertEnabled, + alertGB = d.AlertGB, + belowThreshold = d.BelowThreshold + }; + } + // ─────────────────────────────────────────────────────── helpers ── - /// 定位日志根:优先 appsettings Logs:Root,否则取 SimpleLite 工作目录下的 log + /// 定位日志根:优先 appsettings Logs:Root,否则取 Simple3 工作目录下的 log private (string? root, string? workdir, string? error) ResolveLogRoot() { var overrideRoot = _config["Logs:Root"]; @@ -607,7 +655,7 @@ public sealed class LogsController : ControllerBase var wd = _launcher.ResolveWorkingDirectory(); if (string.IsNullOrWhiteSpace(wd)) return (null, null, - "未能定位 SimpleLite 工作目录,无法读取日志。请在 appsettings.json 配置 SimpleLite:WorkingDirectory," + + "未能定位 Simple3 工作目录,无法读取日志。请在 appsettings.json 配置 Simple3:WorkingDirectory," + "或显式设置 Logs:Root 指向日志根目录。"); return (Path.GetFullPath(Path.Combine(wd, "log")), wd, null); diff --git a/MiGu.Server/Logs/LogCleanupService.cs b/MiGu.Server/Logs/LogCleanupService.cs new file mode 100644 index 0000000..8978335 --- /dev/null +++ b/MiGu.Server/Logs/LogCleanupService.cs @@ -0,0 +1,270 @@ +using MiGu.Server.Configs; +using MiGu.Server.Launcher; + +namespace MiGu.Server.Logs; + +/// +/// 对齐 Simple3 LogCleaner:定时删除 log/ 下过期 *.log,并检测日志盘剩余空间(边沿告警,写日志不弹窗)。 +/// +public sealed class LogCleanupService : IHostedService, IDisposable +{ + private readonly ConfigStore _store; + private readonly Simple3Launcher _launcher; + private readonly IConfiguration _config; + private readonly ILogger _log; + + private readonly object _cleanLock = new(); + private readonly object _scheduleLock = new(); + private Timer? _cleanTimer; + private Timer? _diskTimer; + private bool _diskBelow; + + public LogCleanupService( + ConfigStore store, + Simple3Launcher launcher, + IConfiguration config, + ILogger log) + { + _store = store; + _launcher = launcher; + _config = config; + _log = log; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + try + { + var opt = _store.GetLogCleanup(); + if (opt.Enabled && opt.RunOnStartup) + RunCleanSafely(opt, "启动清理"); + ApplySchedule(); + CheckDisk(opt); + } + catch (Exception ex) + { + _log.LogWarning(ex, "LogCleanupService 启动失败"); + } + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + lock (_scheduleLock) + { + _cleanTimer?.Dispose(); + _cleanTimer = null; + _diskTimer?.Dispose(); + _diskTimer = null; + } + return Task.CompletedTask; + } + + public void Dispose() + { + _cleanTimer?.Dispose(); + _diskTimer?.Dispose(); + } + + public LogCleanupOptions GetOptions() => _store.GetLogCleanup(); + + public LogCleanupOptions SaveOptions(LogCleanupOptions options) + { + var saved = _store.PutLogCleanup(options); + _ = saved; + var opt = _store.GetLogCleanup(); + ApplySchedule(); + CheckDisk(opt, forceReeval: true); + return opt; + } + + public void ApplySchedule() + { + var opt = _store.GetLogCleanup(); + lock (_scheduleLock) + { + _cleanTimer?.Dispose(); + _cleanTimer = null; + if (opt.Enabled) + { + var hours = opt.CheckIntervalHours <= 0 ? 1 : opt.CheckIntervalHours; + var period = TimeSpan.FromHours(hours); + _cleanTimer = new Timer(_ => RunCleanSafely(_store.GetLogCleanup(), "定时清理"), + null, period, period); + } + + _diskTimer?.Dispose(); + var mins = opt.DiskCheckIntervalMinutes <= 0 ? 30 : opt.DiskCheckIntervalMinutes; + var diskPeriod = TimeSpan.FromMinutes(mins); + _diskTimer = new Timer(_ => CheckDisk(_store.GetLogCleanup()), + null, diskPeriod, diskPeriod); + } + } + + public CleanResult CleanNow() + { + var opt = _store.GetLogCleanup(); + var r = CleanOnce(opt); + if (r.Error != null) + _log.LogWarning("立即清理失败:{Error}", r.Error); + else if (r.DeletedFiles > 0) + _log.LogInformation("立即清理删除 {Count} 个日志文件,释放 {Mb:F1} MB", r.DeletedFiles, r.FreedMB); + else + _log.LogInformation("立即清理完成:没有过期日志"); + return r; + } + + public DiskStatus GetDiskStatus() + { + var opt = _store.GetLogCleanup(); + var known = TryGetFreeGB(out var freeGB, out var drive); + var below = known && opt.DiskAlertEnabled && freeGB < opt.DiskFreeAlertGB; + return new DiskStatus(known, drive, freeGB, opt.DiskAlertEnabled, opt.DiskFreeAlertGB, below); + } + + private void RunCleanSafely(LogCleanupOptions opt, string reason) + { + if (!opt.Enabled && reason != "立即清理") return; + var r = CleanOnce(opt); + if (r.Error != null) + _log.LogWarning("[{Reason}] 失败:{Error}", reason, r.Error); + else if (r.DeletedFiles > 0) + _log.LogInformation("[{Reason}] 删除 {Count} 个日志文件,释放 {Mb:F1} MB", reason, r.DeletedFiles, r.FreedMB); + } + + private CleanResult CleanOnce(LogCleanupOptions opt) + { + opt.Clamp(); + lock (_cleanLock) + { + try + { + var (root, _, error) = ResolveLogRoot(); + if (root == null) + return new CleanResult(0, 0, error ?? "无法定位日志目录"); + if (!Directory.Exists(root)) + return new CleanResult(0, 0, null); + + var cutoff = DateTime.Now.AddDays(-opt.RetentionDays); + var deleted = 0; + long freed = 0; + foreach (var f in Directory.EnumerateFiles(root, "*.log", SearchOption.AllDirectories)) + { + try + { + var fi = new FileInfo(f); + if (fi.LastWriteTime >= cutoff) continue; + var len = fi.Length; + fi.Delete(); + deleted++; + freed += len; + } + catch + { + /* 占用/权限:跳过 */ + } + } + TryRemoveEmptyDirs(root); + return new CleanResult(deleted, freed, null); + } + catch (Exception ex) + { + return new CleanResult(0, 0, ex.Message); + } + } + } + + private static void TryRemoveEmptyDirs(string root) + { + try + { + foreach (var dir in Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories) + .OrderByDescending(d => d.Length)) + { + try + { + if (!Directory.EnumerateFileSystemEntries(dir).Any()) + Directory.Delete(dir, false); + } + catch { /* 忽略单目录失败 */ } + } + } + catch { /* 忽略枚举失败 */ } + } + + private void CheckDisk(LogCleanupOptions opt, bool forceReeval = false) + { + if (forceReeval) _diskBelow = false; + if (!opt.DiskAlertEnabled) + { + _diskBelow = false; + return; + } + + if (!TryGetFreeGB(out var freeGB, out var drive)) + return; + + if (freeGB < opt.DiskFreeAlertGB) + { + if (_diskBelow) return; + _diskBelow = true; + _log.LogWarning( + "磁盘剩余空间不足:{Drive} 仅剩 {Free:F1} GB(低于阈值 {Threshold:F0} GB)。请及时清理磁盘,或在系统配置中调低日志保留天数 / 立即清理。", + drive, freeGB, opt.DiskFreeAlertGB); + } + else + { + _diskBelow = false; + } + } + + private bool TryGetFreeGB(out double freeGB, out string driveName) + { + freeGB = 0; + driveName = ""; + var (root, _, _) = ResolveLogRoot(); + if (root == null) return false; + try + { + var driveRoot = Path.GetPathRoot(root); + if (string.IsNullOrEmpty(driveRoot)) return false; + var di = new DriveInfo(driveRoot); + driveName = di.Name; + freeGB = di.AvailableFreeSpace / 1024.0 / 1024.0 / 1024.0; + return true; + } + catch + { + return false; + } + } + + private (string? root, string? workdir, string? error) ResolveLogRoot() + { + var overrideRoot = _config["Logs:Root"]; + if (!string.IsNullOrWhiteSpace(overrideRoot)) + { + var r = Path.GetFullPath(overrideRoot); + return (r, Path.GetDirectoryName(r), null); + } + + var wd = _launcher.ResolveWorkingDirectory(); + if (string.IsNullOrWhiteSpace(wd)) + return (null, null, "未能定位 Simple3 工作目录"); + + return (Path.GetFullPath(Path.Combine(wd, "log")), wd, null); + } + + public readonly record struct CleanResult(int DeletedFiles, long FreedBytes, string? Error) + { + public double FreedMB => FreedBytes / 1024.0 / 1024.0; + } + + public readonly record struct DiskStatus( + bool Known, + string Drive, + double FreeGB, + bool AlertEnabled, + double AlertGB, + bool BelowThreshold); +} diff --git a/MiGu.Server/data/config-system.json b/MiGu.Server/data/config-system.json index f37b3fe..9b00263 100644 --- a/MiGu.Server/data/config-system.json +++ b/MiGu.Server/data/config-system.json @@ -1,7 +1,7 @@ { "section": "system", - "version": 1, - "updatedAt": "2026-06-08T09:43:37.4470202+00:00", + "version": 2, + "updatedAt": "2026-08-21T08:06:13.7109039+00:00", "payload": { "dispatchLoopHz": 50, "log": { @@ -11,7 +11,7 @@ }, "security": { "jwtExpireMin": 1440, - "enableSwagger": false, + "enableSwagger": true, "corsWhitelist": [ "http://localhost:5173" ] diff --git a/frontends/apps/simple-platform-vue/src/api/logs.ts b/frontends/apps/simple-platform-vue/src/api/logs.ts index e5ec873..282275f 100644 --- a/frontends/apps/simple-platform-vue/src/api/logs.ts +++ b/frontends/apps/simple-platform-vue/src/api/logs.ts @@ -3,7 +3,7 @@ import http from './http' /** * 「日志管理」前端胶水:对应 MiGu.Server `LogsController`(`/api/logs/*`)。 * - * 数据是 SimpleLite 内核 `Diagnosis.Post / Diagnosis.Log` 写到工作目录 `log/{日期}/xxx.log` 的落盘日志 + * 数据是 Simple3 内核 `Diagnosis.Post / Diagnosis.Log` 写到工作目录 `log/{日期}/xxx.log` 的落盘日志 * (俗称 DLog)。后端直接读文件并解析为结构化条目,提供:概览 / 文件列表 / 条目分页 / * 「按标签合订」/ 原文 / 下载。 * @@ -14,10 +14,10 @@ import http from './http' const MOCK = import.meta.env.VITE_USE_MOCK === 'true' const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api' -/** SimpleLite projection 投影 API 统一信封(同 reflection.ts)。 */ +/** Simple3 projection 投影 API 统一信封(同 reflection.ts)。 */ interface SlEnvelope { success: boolean; code: number; data: T | null; message: string } -/** 经 YARP 反代到 SimpleLite EmbedIO 的诊断投影端点(/api/sl/projection/diagnosis)。 */ +/** 经 YARP 反代到 Simple3 EmbedIO 的诊断投影端点(/api/sl/projection/diagnosis)。 */ const SL_DIAG = '/sl/projection/diagnosis' export interface LogEntry { @@ -47,6 +47,31 @@ export interface LogDayStat { bytes: number } +export interface LogDiskStatus { + known: boolean + drive: string + freeGB: number + alertEnabled: boolean + alertGB: number + belowThreshold: boolean +} + +export interface LogCleanupConfig { + enabled: boolean + retentionDays: number + checkIntervalHours: number + runOnStartup: boolean + diskAlertEnabled: boolean + diskFreeAlertGB: number + diskCheckIntervalMinutes: number +} + +export interface LogCleanupResult { + deletedFiles: number + freedBytes: number + freedMB: number +} + export interface LogOverview { exists: boolean root: string | null @@ -56,6 +81,7 @@ export interface LogOverview { latestFileTime?: string | null days?: LogDayStat[] message?: string + disk?: LogDiskStatus } export interface LogFilesResult { @@ -116,7 +142,7 @@ export interface DigestQuery { maxPerTag?: number } -/** 实时诊断单条:对应 SimpleLite 内核 Diagnosis 的内存态 Post/Toast。 */ +/** 实时诊断单条:对应 Simple3 内核 Diagnosis 的内存态 Post/Toast。 */ export interface LiveDiagItem { index: number time: string @@ -225,12 +251,25 @@ export interface AnalyzeQuery { function mockOverview(): LogOverview { return { exists: true, - root: 'E:\\...\\SimpleLite\\bin\\Debug\\log', - workingDirectory: 'E:\\...\\SimpleLite\\bin\\Debug', + root: 'E:\\...\\Simple3\\bin\\Debug\\log', + workingDirectory: 'E:\\...\\Simple3\\bin\\Debug', totalFiles: 3, totalBytes: 5_233_649, latestFileTime: new Date().toISOString(), - days: [{ day: '2026-06-01', files: 3, bytes: 5_233_649 }] + days: [{ day: '2026-06-01', files: 3, bytes: 5_233_649 }], + disk: { known: true, drive: 'E:\\', freeGB: 118.7, alertEnabled: true, alertGB: 5, belowThreshold: false } + } +} + +function mockCleanupConfig(): LogCleanupConfig { + return { + enabled: true, + retentionDays: 30, + checkIntervalHours: 24, + runOnStartup: true, + diskAlertEnabled: true, + diskFreeAlertGB: 5, + diskCheckIntervalMinutes: 30 } } @@ -285,7 +324,7 @@ function mockLiveDiagnosis(): LiveDiagnosis { { index: 0, time: iso(2), tag: 'Persistence', tagged: true, content: 'flush 4 entities ok' }, { index: 1, time: iso(4), tag: 'Dispatch', tagged: true, content: 'dispatch loop 50Hz online' }, { index: 2, time: iso(6), tag: 'InternalAuth', tagged: true, content: '仅放行本机回环(无 internal token 配置)' }, - { index: 3, time: iso(1), tag: '', tagged: false, content: '[SimpleLite] Projection API listening on http://127.0.0.1:8222/projection/' }, + { index: 3, time: iso(1), tag: '', tagged: false, content: '[Simple3] Projection API listening on http://127.0.0.1:8222/projection/' }, { index: 4, time: iso(8), tag: '', tagged: false, content: 'loaded 12 sites, 18 tracks' } ] const taggedCount = items.filter((i) => i.tagged).length @@ -373,7 +412,7 @@ export const logsApi = { ? Promise.resolve(mockDigest()) : http.get('/logs/digest', { params: q }).then((r) => r.data), - /** 实时内存诊断(SimpleLite Diagnosis.GetAllDiagnosis,经 YARP 反代)。需 SimpleLite 在运行,否则 502。 */ + /** 实时内存诊断(Simple3 Diagnosis.GetAllDiagnosis,经 YARP 反代)。需 Simple3 在运行,否则 502。 */ liveDiagnosis: (): Promise => MOCK ? Promise.resolve(mockLiveDiagnosis()) : http.get>(`${SL_DIAG}/all`).then((r) => { @@ -398,5 +437,17 @@ export const logsApi = { /** 浏览器直链下载(GET,靠 httpOnly Cookie 鉴权)。 */ downloadUrl: (file: string): string => - `${API_BASE}/logs/download?file=${encodeURIComponent(file)}` + `${API_BASE}/logs/download?file=${encodeURIComponent(file)}`, + + cleanupConfig: (): Promise => MOCK + ? Promise.resolve(mockCleanupConfig()) + : http.get('/logs/cleanup-config').then((r) => r.data), + + saveCleanupConfig: (body: LogCleanupConfig): Promise => MOCK + ? Promise.resolve({ ...body }) + : http.put('/logs/cleanup-config', body).then((r) => r.data), + + cleanupNow: (): Promise => MOCK + ? Promise.resolve({ deletedFiles: 0, freedBytes: 0, freedMB: 0 }) + : http.post('/logs/cleanup-now').then((r) => r.data) } diff --git a/frontends/apps/simple-platform-vue/src/types/config.ts b/frontends/apps/simple-platform-vue/src/types/config.ts index 9b192a9..5063288 100644 --- a/frontends/apps/simple-platform-vue/src/types/config.ts +++ b/frontends/apps/simple-platform-vue/src/types/config.ts @@ -2,22 +2,7 @@ * 配置中心 13+1 维度强类型(与 Platform.Server/Configs 一一对齐,字段命名追随 ARCHITECTURE.md §9)。 */ -export interface LogPolicy { - level: 'trace' | 'debug' | 'info' | 'warn' | 'error' - rollDays: number - maxSizeMB: number -} - -export interface SecurityPolicy { - jwtExpireMin: number - enableSwagger: boolean - corsWhitelist: string[] -} - export interface SystemConfig { - dispatchLoopHz: number - log: LogPolicy - security: SecurityPolicy } export interface MesEndpoint { id: string; name: string; url: string; enabled: boolean } diff --git a/frontends/apps/simple-platform-vue/src/views/admin/LogManagementView.vue b/frontends/apps/simple-platform-vue/src/views/admin/LogManagementView.vue index 69bdfe4..0b72133 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/LogManagementView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/LogManagementView.vue @@ -9,17 +9,32 @@ 日志管理 Diagnosis · Post / Toast / DLog - 刷新 +
+ 日志清理配置 + 立即清理 + 刷新 +

- 实时诊断直连 SimpleLite 内核 Diagnosis 的内存态 Post / Toast + 实时诊断直连 Simple3 内核 Diagnosis 的内存态 Post / Toast (有标签按标签合订、无标签滚动记录);日志文件浏览工作目录 log/ 下的落盘日志(DLog)。

+
日志根目录{{ overview.root ?? '—' }}
文件数{{ overview.totalFiles ?? 0 }}
总大小{{ formatBytes(overview.totalBytes ?? 0) }}
最近写入{{ overview.latestFileTime ? formatTime(overview.latestFileTime) : '—' }}
+
+ 日志所在盘剩余 + {{ diskText }} +
@@ -74,7 +89,7 @@ @@ -303,10 +318,11 @@