109 lines
3.5 KiB
C#
109 lines
3.5 KiB
C#
using System.Text.Json;
|
|||
|
|
using MAF1.Agents;
|
||
|
|
|
||
|
|
namespace MAF1.Route;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 方案 C 的图:节点 = 主管能调的专家白名单;边 = 允许的交接。
|
||
|
|
/// 没有边时,名单上的节点可以按任意顺序调用。
|
||
|
|
/// </summary>
|
||
|
|
public sealed class RouteGraph
|
||
|
|
{
|
||
|
|
public List<RouteNode> Nodes { get; set; } = [];
|
||
|
|
public List<RouteEdge> Edges { get; set; } = [];
|
||
|
|
|
||
|
|
public bool HasEdges => Edges.Count > 0;
|
||
|
|
|
||
|
|
/// <summary>主管第一跳只能到这些节点:无边时整张白名单;有边时取没有入边的入口。</summary>
|
||
|
|
public IReadOnlyList<RouteNode> EntryNodes()
|
||
|
|
{
|
||
|
|
if (!HasEdges)
|
||
|
|
{
|
||
|
|
return Nodes;
|
||
|
|
}
|
||
|
|
|
||
|
|
HashSet<string> targets = new(Edges.Select(e => e.To), StringComparer.OrdinalIgnoreCase);
|
||
|
|
List<RouteNode> sources = Nodes.Where(n => !targets.Contains(n.Id)).ToList();
|
||
|
|
return sources.Count > 0 ? sources : Nodes;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool HasOutgoing(string nodeId)
|
||
|
|
=> Edges.Any(e => e.From.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
|
||
|
|
|
||
|
|
public RouteNode? Find(string nodeId)
|
||
|
|
=> Nodes.FirstOrDefault(n => n.Id.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
|
||
|
|
|
||
|
|
public static JsonSerializerOptions JsonOptions { get; } = new()
|
||
|
|
{
|
||
|
|
PropertyNameCaseInsensitive = true,
|
||
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||
|
|
WriteIndented = true,
|
||
|
|
};
|
||
|
|
|
||
|
|
public static RouteGraph LoadFile(string path)
|
||
|
|
{
|
||
|
|
string json = File.ReadAllText(path);
|
||
|
|
RouteGraph graph = JsonSerializer.Deserialize<RouteGraph>(json, JsonOptions)
|
||
|
|
?? throw new InvalidOperationException($"无法解析路由图:{path}");
|
||
|
|
graph.Validate();
|
||
|
|
return graph;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Validate()
|
||
|
|
{
|
||
|
|
if (Nodes.Count == 0)
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException("图里没有任何节点。方案 C 至少要放一个专家到白名单。");
|
||
|
|
}
|
||
|
|
|
||
|
|
HashSet<string> ids = new(StringComparer.OrdinalIgnoreCase);
|
||
|
|
foreach (RouteNode node in Nodes)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(node.Id))
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException("节点缺少 id。");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!ids.Add(node.Id))
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException($"节点 id 重复:{node.Id}");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!SystemNodeTypes.IsKnown(node.Type))
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException(
|
||
|
|
$"节点 `{node.Id}` 的类型 `{node.Type}` 不是系统专家。当前支持 {SystemNodeTypes.FileCity} / {SystemNodeTypes.Weather}。");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (RouteEdge edge in Edges)
|
||
|
|
{
|
||
|
|
if (Find(edge.From) is null)
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException($"边的 from `{edge.From}` 不在图上。");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Find(edge.To) is null)
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException($"边的 to `{edge.To}` 不在图上。");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public sealed class RouteNode
|
||
|
|
{
|
||
|
|
public string Id { get; set; } = "";
|
||
|
|
public string Type { get; set; } = "";
|
||
|
|
public string Title { get; set; } = "";
|
||
|
|
public Dictionary<string, string> Config { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||
|
|
}
|
||
|
|
|
||
|
|
public sealed class RouteEdge
|
||
|
|
{
|
||
|
|
public string From { get; set; } = "";
|
||
|
|
public string To { get; set; } = "";
|
||
|
|
/// <summary>可选。hasValidCities / !hasValidCities / 空=只要拓扑允许就可以走。</summary>
|
||
|
|
public string? When { get; set; }
|
||
|
|
}
|