chore: initial project import

This commit is contained in:
justme committed 2026-10-04 21:46:39 +08:00
commit 3fc4e5ca53
347 files changed
+56172

No files matched your search

@@ -0,0 +1,104 @@
using Fleet.Core;
using Monitor.Protocol;
namespace Monitor.Backend.Application;
/// <summary>一条线路 = 一对(Agent,Probe)。存储里它的序列是 <c>line:&lt;probe_id&gt;</c>,对外的 id 是 <c>&lt;agent_id&gt;~&lt;probe_id&gt;</c>。</summary>
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;
}
}
/// <summary>一条线路的展示视图:Agent 的状态、Probe 记录,以及这一对现在是否连着。</summary>
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);
}
/// <summary>线路目录:把 id 解析成(Agent,Probe),列出所有线路。</summary>
public sealed class LineService
{
private readonly AgentService _agents;
private readonly IProbeStore _probes;
public LineService(AgentService agents, IProbeStore probes)
{
_agents = agents;
_probes = probes;
}
/// <summary>所有线路:每个 Agent 和每个<b>未吊销</b>的 Probe 配成一对。</summary>
public async ValueTask<IReadOnlyList<LineView>> ListAsync(Func<AgentView, bool> include, CancellationToken ct)
{
IReadOnlyList<AgentView> agents = await _agents.ListAsync(ct);
IReadOnlyList<ProbeRecord> probes = [.. (await _probes.ListAsync(ct)).Where(p => p.RevokedAtMs is null)];
return [.. agents.Where(include).SelectMany(a => probes.Select(p => new LineView(a, p)))];
}
/// <summary>
/// 线路 id 是 <c>agent~probe</c>。只给 Agent id 时:恰好有一个未吊销的 Probe 就用它,否则要求明确指定(<c>probe_required</c>)。
/// </summary>
public async ValueTask<Result<LineView>> 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<LineView>.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<LineView>.Fail(MonitorErrors.AgentNotFound, "line not found");
}
IReadOnlyList<ProbeRecord> 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<LineView>.Fail(MonitorErrors.ProbeNotFound, "probe not found");
}
}
else
{
ProbeRecord[] active = [.. probes.Where(p => p.RevokedAtMs is null)];
if (active.Length != 1)
{
return Result<LineView>.Fail(MonitorErrors.ProbeRequired, active.Length == 0 ? "there is no active probe" : "several probes exist: use <agent_id>~<probe_id> as the line id");
}
probe = active[0];
}
return Result<LineView>.Ok(new LineView(agent, probe));
}
}