feat(server): 添加静态状态页与调试入口

- 将状态页、调试页改为 `wwwroot` 静态资源
  - 补充调试配置接口与前端脚本
  - 为兼容层、规划层和运行时补充日志
  - 更新集成测试覆盖新入口
This commit is contained in:
2026-04-29 14:05:02 +08:00
parent 0724efebed
commit c38faddbf0
27 changed files with 2630 additions and 1894 deletions

View File

@@ -2,6 +2,7 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using Flyshot.Core.Config;
using Flyshot.Core.Domain;
using Microsoft.Extensions.Logging;
namespace Flyshot.ControllerClientCompat;
@@ -41,16 +42,19 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
{
private readonly ControllerClientCompatOptions _options;
private readonly RobotConfigLoader _configLoader;
private readonly ILogger<JsonFlyshotTrajectoryStore>? _logger;
/// <summary>
/// 初始化基于 JSON 文件的轨迹存储。
/// </summary>
/// <param name="options">兼容层基础配置,用于定位工作区根目录。</param>
/// <param name="configLoader">旧版 RobotConfig.json 加载器,用于反序列化已保存的轨迹。</param>
public JsonFlyshotTrajectoryStore(ControllerClientCompatOptions options, RobotConfigLoader configLoader)
/// <param name="logger">日志记录器;允许 null。</param>
public JsonFlyshotTrajectoryStore(ControllerClientCompatOptions options, RobotConfigLoader configLoader, ILogger<JsonFlyshotTrajectoryStore>? logger = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader));
_logger = logger;
}
/// <inheritdoc />
@@ -59,6 +63,12 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(trajectory);
_logger?.LogInformation(
"TrajectoryStore 保存轨迹: robot={RobotName}, name={TrajectoryName}, waypoints={WaypointCount}",
robotName,
trajectory.Name,
trajectory.Waypoints.Count);
var path = ResolveStorePath(robotName);
var directory = Path.GetDirectoryName(path)!;
Directory.CreateDirectory(directory);
@@ -92,6 +102,8 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
File.WriteAllText(path, root.ToJsonString(writeOptions));
_logger?.LogInformation("TrajectoryStore 轨迹已保存到 {Path}", path);
}
/// <inheritdoc />
@@ -102,9 +114,12 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
throw new ArgumentException("轨迹名称不能为空。", nameof(trajectoryName));
}
_logger?.LogInformation("TrajectoryStore 删除轨迹: robot={RobotName}, name={TrajectoryName}", robotName, trajectoryName);
var path = ResolveStorePath(robotName);
if (!File.Exists(path))
{
_logger?.LogWarning("TrajectoryStore 删除失败: 文件不存在 {Path}", path);
return;
}
@@ -112,19 +127,27 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
var root = JsonNode.Parse(existingJson)?.AsObject();
if (root is null)
{
_logger?.LogWarning("TrajectoryStore 删除失败: 无法解析 JSON {Path}", path);
return;
}
if (root.TryGetPropertyValue("flying_shots", out var flyingShotsNode) && flyingShotsNode is JsonObject flyingShotsObj)
{
flyingShotsObj.Remove(trajectoryName);
var writeOptions = new JsonSerializerOptions
var removed = flyingShotsObj.Remove(trajectoryName);
if (removed)
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
File.WriteAllText(path, root.ToJsonString(writeOptions));
var writeOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
File.WriteAllText(path, root.ToJsonString(writeOptions));
_logger?.LogInformation("TrajectoryStore 轨迹已删除: {TrajectoryName}", trajectoryName);
}
else
{
_logger?.LogWarning("TrajectoryStore 删除失败: 轨迹不存在 {TrajectoryName}", trajectoryName);
}
}
}
@@ -134,12 +157,15 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
var path = ResolveStorePath(robotName);
if (!File.Exists(path))
{
_logger?.LogInformation("TrajectoryStore 无持久化数据: {Path}", path);
settings = null;
return new Dictionary<string, ControllerClientCompatUploadedTrajectory>(StringComparer.Ordinal);
}
try
{
_logger?.LogInformation("TrajectoryStore 正在加载: {Path}", path);
var workspaceRoot = ResolveWorkspaceRoot();
var loaded = _configLoader.Load(path, workspaceRoot);
settings = loaded.Robot;
@@ -156,10 +182,18 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
dict[program.Key] = traj;
}
_logger?.LogInformation(
"TrajectoryStore 加载完成: robot={RobotName}, 轨迹数={Count}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}",
robotName,
dict.Count,
settings?.UseDo,
settings?.IoKeepCycles);
return dict;
}
catch
catch (Exception ex)
{
_logger?.LogError(ex, "TrajectoryStore 加载失败: {Path}", path);
settings = null;
return new Dictionary<string, ControllerClientCompatUploadedTrajectory>(StringComparer.Ordinal);
}