using Fleet.Core; using Monitor.Protocol; namespace Monitor.Backend.Application; /// 一条线路 = 一对(Agent,Probe)。存储里它的序列是 line:<probe_id>,对外的 id 是 <agent_id>~<probe_id>。 public readonly record struct LineKey(NodeId Agent, ProbeId Probe) { public string Series => RawChunkHeader.LinePrefix + Probe.Value; public string Id => Agent.Value + "~" + Probe.Value; public static bool TryParse(string text, out LineKey key) { int split = text.IndexOf('~', StringComparison.Ordinal); if (split <= 0 || split == text.Length - 1) { key = default; return false; } key = new LineKey(new NodeId(text[..split]), new ProbeId(text[(split + 1)..])); return true; } } /// 一条线路的展示视图:Agent 的状态、Probe 记录,以及这一对现在是否连着。 public sealed record LineView(AgentView Agent, ProbeRecord Probe) { public LineKey Key => new(Agent.Agent.Id, new ProbeId(Probe.Id)); public bool Connected => Agent.Probes.Any(p => p.Id == Probe.Id); public ConnectedProbeInfo? Connection => Agent.Probes.FirstOrDefault(p => p.Id == Probe.Id); } /// 线路目录:把 id 解析成(Agent,Probe),列出所有线路。 public sealed class LineService { private readonly AgentService _agents; private readonly IProbeStore _probes; private readonly LinkService _links; public LineService(AgentService agents, IProbeStore probes, LinkService links) { _links = links; _agents = agents; _probes = probes; } /// 所有线路:每个 Agent 和每个未吊销的 Probe 配成一对,只留下线路开着的(见 );关掉的线路历史数据还在,按 id 仍然查得到。 public async ValueTask> ListAsync(Func include, CancellationToken ct) { IReadOnlyList agents = await _agents.ListAsync(ct); IReadOnlyList probes = [.. (await _probes.ListAsync(ct)).Where(p => p.RevokedAtMs is null)]; LinkSnapshot links = await _links.SnapshotAsync(ct); return [.. agents.Where(include).SelectMany(a => probes.Where(p => links.IsLinked(p.Id, a.Agent.Id.Value)).Select(p => new LineView(a, p)))]; } /// /// 线路 id 是 agent~probe。只给 Agent id 时:恰好有一个未吊销的 Probe 就用它,否则要求明确指定(probe_required)。 /// public async ValueTask> ResolveAsync(string id, CancellationToken ct) { string agentPart = id; string? probePart = null; if (id.Contains('~', StringComparison.Ordinal)) { if (!LineKey.TryParse(id, out LineKey key)) { return Result.Fail(MonitorErrors.AgentNotFound, "line not found"); } agentPart = key.Agent.Value; probePart = key.Probe.Value; } AgentView? agent = await _agents.GetAsync(new NodeId(agentPart), ct); if (agent is null) { return Result.Fail(MonitorErrors.AgentNotFound, "line not found"); } IReadOnlyList probes = await _probes.ListAsync(ct); ProbeRecord? probe; if (probePart is not null) { probe = probes.FirstOrDefault(p => p.Id == probePart); if (probe is null) { return Result.Fail(MonitorErrors.ProbeNotFound, "probe not found"); } } else { ProbeRecord[] active = [.. probes.Where(p => p.RevokedAtMs is null)]; if (active.Length != 1) { return Result.Fail(MonitorErrors.ProbeRequired, active.Length == 0 ? "there is no active probe" : "several probes exist: use ~ as the line id"); } probe = active[0]; } return Result.Ok(new LineView(agent, probe)); } }