namespace MoShiro.Backend.Application; /// 某一时刻的「线路」开关:自动连接的总开关,加上每一对(拨号方,监听方)的手动设置。 public sealed class LinkSnapshot { private readonly Dictionary<(string Dialer, string Listener), bool> _overrides; private readonly HashSet _domestic; public LinkSnapshot(bool auto, Dictionary<(string Dialer, string Listener), bool> overrides, HashSet? domestic = null) { Auto = auto; _overrides = overrides; _domestic = domestic ?? []; } /// 自动连接:开着时,每一对节点自动连成一条线路;关着时,只有手动开启的才连。 public bool Auto { get; } /// 手动设置:true 强制连、false 强制不连、null 没设置(跟随自动连接)。只看这一个方向。 public bool? OverrideOf(string dialerId, string listenerId) => _overrides.TryGetValue((dialerId, listenerId), out bool value) ? value : null; /// 这一对节点(不分方向)的手动设置:当前方向有设置就用它,否则看反方向。 public bool? PairOverride(string dialerId, string listenerId) => OverrideOf(dialerId, listenerId) ?? OverrideOf(listenerId, dialerId); /// /// 谁更适合当被连接的一方(越大越适合):不监听的是 0;监听的是 2,但国内(国家 CN)的监听节点是 1——国内节点默认只拨出, /// 除非对端没有更好的选择。手动指定的方向不受这个影响。 /// private int ListenerRank(AgentRecord node) => !node.Listens ? 0 : _domestic.Contains(node.Id.Value) ? 1 : 2; /// /// 自动连接时这一对由谁拨:更适合当被连接方的一方被拨;一样适合时 id 较小的一方拨。 /// 所以不监听的拨监听的,国内的拨国外的,其余按 id。一对节点只有一条线路、一个连接、一份 RTT;上下行都在这条连接上测。 /// public bool IsDefaultDialer(AgentRecord dialer, AgentRecord listener) { if (!listener.Listens) { return false; } int rank = ListenerRank(listener) - ListenerRank(dialer); return rank > 0 || (rank == 0 && string.CompareOrdinal(dialer.Id.Value, listener.Id.Value) < 0); } /// /// 这一对最终连不连、是不是由这个方向拨:手动设置优先(true 的方向就是拨号方,两个方向都设了 true 时按默认方向), /// 对端不监听时反方向拨不了;有 false 就整对不连;没有设置就看自动连接和默认方向。 /// public bool IsLinked(AgentRecord dialer, AgentRecord listener) { if (dialer.Id == listener.Id || !listener.Listens) { return false; } bool? forward = OverrideOf(dialer.Id.Value, listener.Id.Value); bool? reverse = OverrideOf(listener.Id.Value, dialer.Id.Value); if (forward == true) { return reverse != true || !dialer.Listens || IsDefaultDialer(dialer, listener); } if (reverse == true && dialer.Listens) { return false; } if (forward == false || reverse == false) { return false; } return Auto && IsDefaultDialer(dialer, listener); } } /// /// 哪些(拨号方,监听方)要连成线路。由面板决定、拨号方主动拨出:每一对节点只有一条线路,自动连接开着时都连(谁拨见 ,国内节点默认只拨出), /// 关着时只连手动选中的;每一对还可以手动强制开或关,并且用 true 选定由哪一方拨。手动设置存在设置表里(link.<拨号方>.<监听方>,值 1 或 0,空表示没设置),一对节点最多保留一条。 /// 判断结果分别用在拨号方的拨号目标、监听方允许的拨号方(peers)和线路目录里;改动之后要调用 重新下发。 /// public sealed class LinkService { private const string Prefix = "link."; private readonly ISettingsStore _settings; private readonly NodeInfoService _nodes; public LinkService(ISettingsStore settings, NodeInfoService nodes) { _settings = settings; _nodes = nodes; } public async ValueTask SnapshotAsync(CancellationToken ct) { IReadOnlyDictionary all = await _settings.GetAllAsync(ct); var overrides = new Dictionary<(string, string), bool>(); foreach ((string key, string value) in all) { string[] parts = key.Split('.'); if (parts.Length == 3 && parts[0] + "." == Prefix && value is "1" or "0") { overrides[(parts[1], parts[2])] = value == "1"; } } HashSet domestic = [.. (await _nodes.ListAsync(ct)).Where(n => n.Country == "CN").Select(n => n.NodeId)]; return new LinkSnapshot(all.GetValueOrDefault(SettingsService.AutoLink) != "false", overrides, domestic); } /// 手动设置一对线路:true 强制连并由 拨、false 强制不连、null 取消手动设置。反方向的旧设置同时清掉。 public async ValueTask SetAsync(string dialerId, string listenerId, bool? enabled, CancellationToken ct) { await _settings.SetAsync($"{Prefix}{listenerId}.{dialerId}", "", ct); await _settings.SetAsync($"{Prefix}{dialerId}.{listenerId}", enabled is null ? "" : enabled.Value ? "1" : "0", ct); } /// 恢复默认连接模式:清掉所有手动设置并打开自动连接,一对节点谁拨谁完全按默认规则。 public async ValueTask ResetAsync(CancellationToken ct) { foreach (string key in (await _settings.GetAllAsync(ct)).Where(kv => kv.Key.StartsWith(Prefix, StringComparison.Ordinal) && kv.Value.Length > 0).Select(kv => kv.Key)) { await _settings.SetAsync(key, "", ct); } await _settings.SetAsync(SettingsService.AutoLink, "true", ct); } /// 节点被删除:清掉所有涉及它的手动设置。 public async ValueTask ForgetNodeAsync(string nodeId, CancellationToken ct) { foreach (string key in (await _settings.GetAllAsync(ct)).Keys.Where(k => k.StartsWith(Prefix, StringComparison.Ordinal) && k.Split('.').Skip(1).Contains(nodeId))) { await _settings.SetAsync(key, "", ct); } } }