161 lines
8.7 KiB
C#
161 lines
8.7 KiB
C#
using System.Text.Json;
|
|
using Fleet.Server;
|
|
using MoShiro.Backend.Analysis;
|
|
using MoShiro.Backend.Application;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Backend.Api;
|
|
|
|
/// <summary>应用层模型 → REST DTO。public 层的 DTO 只暴露可以公开的字段。</summary>
|
|
public static class Mappers
|
|
{
|
|
public static DateTimeOffset Time(long ms) => DateTimeOffset.FromUnixTimeMilliseconds(ms);
|
|
|
|
public static DateTimeOffset? Time(long? ms) => ms is { } v ? DateTimeOffset.FromUnixTimeMilliseconds(v) : null;
|
|
|
|
public static string StatusName(NodeStatus status) => status switch
|
|
{
|
|
NodeStatus.Active => "active",
|
|
NodeStatus.Disabled => "disabled",
|
|
_ => "pending",
|
|
};
|
|
|
|
public static TestSummaryDto? Summary(TestRecord? test) =>
|
|
test is null ? null : new TestSummaryDto(test.Id.Value, test.Plan, test.Status, Time(test.RequestedAtMs), Time(test.FinishedAtMs));
|
|
|
|
public static SummaryDto ToDto(SummaryResult summary) =>
|
|
new(summary.Values, summary.Samples.ToDictionary(s => s.Key, s => checked((long)s.Value)), summary.Coverage);
|
|
|
|
public static EventDto ToDto(EventRecord e) => new(e.Id, e.AgentId?.Value, e.Kind, Time(e.AtMs), e.Detail);
|
|
|
|
public static OverviewLineDto ToDto(OverviewLine line) =>
|
|
new(
|
|
line.Line.Key.Id, line.Line.Agent.Agent.Id.Value, line.Line.Probe.Id, line.Line.Probe.Name, line.Line.Agent.Agent.Name, line.Line.Agent.Agent.Labels,
|
|
line.Line.Agent.Online, line.Line.Connected, line.Line.Agent.EndpointChanges, ToDto(line.Summary), Summary(line.LastTest), line.Line.IpVersion);
|
|
|
|
public static HostDto ToDto(HostSnapshot h) =>
|
|
new(h.CpuPercent, h.MemUsed, h.MemTotal, h.DiskUsed, h.DiskTotal, h.NetRxTotal, h.NetTxTotal, h.NetRxBps, h.NetTxBps, h.NetLinkMbps, h.UptimeSeconds, Time(h.AtUtcMs));
|
|
|
|
public static ConnectedProbeDto ToDto(ConnectedProbeInfo p) => new(p.Id, p.Version, p.Remote, Time(p.SinceMs), p.IpVersion);
|
|
|
|
public static ProbeDto ToDto(ProbeView v) =>
|
|
new(
|
|
v.Probe.Id, v.Probe.Name, Time(v.Probe.CreatedAtMs), Time(v.Probe.RevokedAtMs), v.Revoked, v.Online,
|
|
[.. v.Connections.Select(c => new ProbeConnectionDto(c.AgentId.Value, c.AgentName, c.AgentOnline, c.Version, c.Remote, Time(c.SinceMs), c.IpVersion))]);
|
|
|
|
public static SeriesDto ToDto(SeriesResult r, bool live) =>
|
|
new(
|
|
r.Line.Id, r.Line.Agent.Value, r.Line.Probe.Value, Time(r.Range.FromMs), Time(r.Range.ToMs), Steps.Format(r.Range.StepMs), r.Plan.Name, live && r.Range.IsRelative, r.Plan.Degraded,
|
|
r.T, r.Values, ToDto(r.Summary), new EdgePartialDto(r.Range.FirstPartial, r.Range.LastPartial),
|
|
[.. r.Gaps.Select(g => new GapDto(Time(g.FromMs), Time(g.ToMs), g.Reason))], [.. r.Events.Select(ToDto)], r.Versions, r.Line.IpVersion);
|
|
|
|
public static TestDto ToDto(TestDetail detail)
|
|
{
|
|
TestRecord t = detail.Test;
|
|
return new TestDto(
|
|
t.Id.Value, t.AgentId.Value, t.ProbeId.Value, t.Plan, detail.PhaseKinds, t.Status, t.Phase, t.ErrorCode, t.Trigger,
|
|
Time(t.RequestedAtMs), Time(t.StartedAtMs), Time(t.FinishedAtMs), detail.Result is null ? null : ToDto(detail.Result), t.IpVersion);
|
|
}
|
|
|
|
public static TestDto ToDto(TestRecord t) =>
|
|
new(t.Id.Value, t.AgentId.Value, t.ProbeId.Value, t.Plan, [.. (Plans.Find(t.Plan)?.Phases ?? []).Select(p => p.Kind)], t.Status, t.Phase, t.ErrorCode, t.Trigger, Time(t.RequestedAtMs), Time(t.StartedAtMs), Time(t.FinishedAtMs), null, t.IpVersion);
|
|
|
|
public static TestResultDto ToDto(TestResultView r) =>
|
|
new(
|
|
r.Metrics,
|
|
r.Scores.ToDictionary(s => s.Key, s => new ScoreDto(s.Value.Score, s.Value.Version, s.Value.Estimated, [.. s.Value.Dimensions.Select(d => new DimensionDto(d.Id, d.Label, d.Weight, d.Value, d.Score))], s.Value.Caveats)),
|
|
r.Series,
|
|
[.. r.Steps.Select(s => new TestStepDto(s.Index, s.TargetMbps, s.LossPct, s.JitterP95Ms, s.DeliveredMbps))],
|
|
r.Caveats);
|
|
|
|
public static AgentDto ToDto(AgentView view)
|
|
{
|
|
AgentRecord a = view.Agent;
|
|
LocalLimitsDto? limits = null;
|
|
if (a.LocalLimitsJson is { } json)
|
|
{
|
|
try
|
|
{
|
|
LocalLimits? parsed = JsonSerializer.Deserialize(json, ApiJsonContext.Default.LocalLimits);
|
|
limits = parsed is null ? null : new LocalLimitsDto(parsed.MaxTotalMbps, parsed.MaxTotalPps, parsed.AllowedTargetCidrs, parsed.MaxConcurrentTests);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// 无法解析就当作没有上报
|
|
}
|
|
}
|
|
|
|
IReadOnlyList<RejectedDto> rejected = [];
|
|
if (a.RejectedJson is { } rejectedJson)
|
|
{
|
|
try
|
|
{
|
|
rejected = [.. (JsonSerializer.Deserialize(rejectedJson, ApiJsonContext.Default.ListRejectedItem) ?? []).Select(r => new RejectedDto(r.Id, r.Reason))];
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// 同上
|
|
}
|
|
}
|
|
|
|
HeartbeatDto? heartbeat = view.Heartbeat is { } h
|
|
? new HeartbeatDto(h.Sessions, h.PpsIn, h.PpsOut, h.DroppedInvalid, h.DroppedRateLimited, h.QueueBytes, Time(view.HeartbeatAtMs ?? 0))
|
|
: null;
|
|
return new AgentDto(
|
|
a.Id.Value, a.Name, StatusName(a.Status), view.Online, view.ProbeConnected, [.. view.Probes.Select(ToDto)], view.Host is null ? null : ToDto(view.Host), a.PublicHost, a.ProbeTcpPort, a.ProbeUdpPorts, a.Labels, a.PublicVisible,
|
|
a.Kinds, a.AgentVersion, limits, a.TaskVersion, a.AppliedTaskVersion, rejected, heartbeat, view.EndpointChanges, Time(a.CreatedAtMs), Time(a.LastSeenAtMs), a.Ipv6Host, a.Ipv6Supported);
|
|
}
|
|
|
|
public static TaskDto ToDto(TaskRecord t) =>
|
|
new(t.Id.Value, t.Scope, t.AgentId?.Value, t.BaseTaskId?.Value, t.Kind, t.Target, t.Params, t.Schedule, t.Enabled, Time(t.UpdatedAtMs));
|
|
|
|
public static JoinTokenDto ToDto(JoinTokenInfo t) =>
|
|
new(t.Id, t.Note, t.Labels, t.AutoApprove, t.MaxUses, t.Uses, Time(t.ExpiresAtMs), Time(t.RevokedAtMs), Time(t.CreatedAtMs));
|
|
|
|
public static ApiTokenDto ToDto(ApiTokenRecord t) => new(t.Id, t.Name, t.Scope, Time(t.CreatedAtMs), Time(t.LastUsedAtMs), Time(t.RevokedAtMs));
|
|
|
|
public static RawChunkMetaDto ToDto(RawChunkMeta m) =>
|
|
new(m.ChunkId.ToString(), m.AgentId.Value, m.Series, m.Kind, m.TaskId?.Value, m.TestId?.Value, m.Phase, Time(m.StartTs), m.DurationMs, m.Partial, m.Length);
|
|
|
|
public static SettingDto ToDto(SettingView s) => new(s.Key, s.Type, s.Value, s.Default, s.Description);
|
|
|
|
public static UserDto ToDto(UserRecord u) => new(u.Id, u.Username, Time(u.CreatedAtMs));
|
|
|
|
// ── 目录 ──
|
|
|
|
public static KindDto ToDto(KindSpec k) =>
|
|
new(
|
|
k.Id, k.Title, k.Description,
|
|
[.. Enum.GetValues<Scope>().Where(s => k.Scopes.HasFlag(s)).Select(s => s.ToString().ToLowerInvariant())],
|
|
new BlocksDto(k.Blocks.Transport, k.Blocks.Pacer, k.Blocks.Payload, k.Blocks.Behavior, k.Blocks.Samplers),
|
|
[.. k.Params.Select(ToDto)],
|
|
[.. k.Streams.Select(s => new StreamDto(s.Name, [.. s.Columns.Select(c => new ColumnDto(c.Name, RawChunkCodec.TypeName(c.Type), c.Enc.ToString().ToLowerInvariant(), c.Nullable, RoleName(c.Role)))]))]);
|
|
|
|
public static ParamDto ToDto(ParamSpec p) => p switch
|
|
{
|
|
IntParam i => new ParamDto(i.Name, "int", i.DefaultValue, i.Min, i.Max, i.Unit, null, i.Description),
|
|
NumberParam n => new ParamDto(n.Name, "number", n.DefaultValue, n.Min, n.Max, n.Unit, null, n.Description),
|
|
BoolParam b => new ParamDto(b.Name, "bool", b.DefaultValue, null, null, null, null, b.Description),
|
|
EnumParam e => new ParamDto(e.Name, "enum", e.DefaultValue, null, null, null, e.Values, e.Description),
|
|
_ => new ParamDto(p.Name, "unknown", p.DefaultValue, null, null, null, null, p.Description),
|
|
};
|
|
|
|
public static PlanDto ToDto(PlanSpec p) =>
|
|
new(p.Id, p.Title, p.Description, [.. p.Phases.Select(ph => new PhaseDto(ph.Kind, ph.DurationSeconds, JsonSerializer.SerializeToElement(ph.Overrides.ToDictionary(o => o.Key, o => o.Value), ApiJsonContext.Default.SettingsPatch)))]);
|
|
|
|
public static MetricDto ToDto(MetricDef def, MetricExport export) =>
|
|
new(
|
|
export.Id, def.Id, def.Kind.ToString().ToLowerInvariant(), Metric.StatName(export.Stat), def.Label, def.Description,
|
|
export.Unit.ToString().ToLowerInvariant(), def.Better.ToString().ToLowerInvariant(), export.Good, export.Poor, def.Fingerprint, def.From);
|
|
|
|
public static ModelDto ToDto(IServiceModel m) => new(m.Id, m.Title, m.Version, m.Needs);
|
|
|
|
private static string? RoleName(ColumnRole role) => role switch
|
|
{
|
|
ColumnRole.TsProbe => "ts_probe",
|
|
ColumnRole.TsAgent => "ts_agent",
|
|
ColumnRole.TsLocal => "ts_local",
|
|
_ => null,
|
|
};
|
|
}
|