web: reset links to the default mode, and keep CN agents dial-only by default [CI]
POST /api/admin/links/reset clears every manual link setting and turns automatic linking on. In the default rules a listening node in CN ranks below a listening node elsewhere, so the CN side dials; the links are pushed again when a node's country is first resolved or changes.
This commit is contained in:
1 parent
35207a70ea
commit
7fe9775f97
8 files changed
+221
-14
No files matched your search
@@ -4491,6 +4491,48 @@
|
|||||||
"x-moshiro-capability": "fleet.read"
|
"x-moshiro-capability": "fleet.read"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/admin/links/reset": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"admin-links"
|
||||||
|
],
|
||||||
|
"summary": "Drop every manual link setting and turn automatic linking on: who dials whom follows the default rules again",
|
||||||
|
"operationId": "links_reset",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LinkMatrixDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-moshiro-capability": "fleet.write"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/admin/links/{dialer_id}/{listener_id}": {
|
"/api/admin/links/{dialer_id}/{listener_id}": {
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public static class LinkEndpoints
|
|||||||
group.MapGet("/links", ListAsync)
|
group.MapGet("/links", ListAsync)
|
||||||
.Api("links_list", "Every possible dialer/listener pair: whether it is linked, the manual override and whether it is connected; plus the nodes and which of them listen", Tag).Returns<LinkMatrixDto>().ProducesProblems(401, 403)
|
.Api("links_list", "Every possible dialer/listener pair: whether it is linked, the manual override and whether it is connected; plus the nodes and which of them listen", Tag).Returns<LinkMatrixDto>().ProducesProblems(401, 403)
|
||||||
.Requires(Capability.FleetRead);
|
.Requires(Capability.FleetRead);
|
||||||
|
group.MapPost("/links/reset", ResetAsync)
|
||||||
|
.Api("links_reset", "Drop every manual link setting and turn automatic linking on: who dials whom follows the default rules again", Tag).Returns<LinkMatrixDto>().ProducesProblems(401, 403)
|
||||||
|
.Requires(Capability.FleetWrite);
|
||||||
group.MapPut("/links/{dialer_id}/{listener_id}", SetAsync)
|
group.MapPut("/links/{dialer_id}/{listener_id}", SetAsync)
|
||||||
.Api("links_set", "Connect a node to a listening node (enabled: true), force the link off (false), or give it back to the automatic setting (null)", Tag).Returns<LinkDto>().ProducesProblems(400, 401, 403, 404, 409, 422)
|
.Api("links_set", "Connect a node to a listening node (enabled: true), force the link off (false), or give it back to the automatic setting (null)", Tag).Returns<LinkDto>().ProducesProblems(400, 401, 403, 404, 409, 422)
|
||||||
.Requires(Capability.FleetWrite);
|
.Requires(Capability.FleetWrite);
|
||||||
@@ -29,6 +32,13 @@ public static class LinkEndpoints
|
|||||||
.Requires(Capability.FleetWrite);
|
.Requires(Capability.FleetWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> ResetAsync(AgentService agents, LinkService links, LinkRuntime runtime, TimeProvider time, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await links.ResetAsync(ct);
|
||||||
|
await agents.AfterAgentSetChangedAsync(ct);
|
||||||
|
return await ListAsync(agents, links, runtime, time, ct);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<IResult> ListAsync(AgentService agents, LinkService links, LinkRuntime runtime, TimeProvider time, CancellationToken ct)
|
private static async Task<IResult> ListAsync(AgentService agents, LinkService links, LinkRuntime runtime, TimeProvider time, CancellationToken ct)
|
||||||
{
|
{
|
||||||
LinkSnapshot snapshot = await links.SnapshotAsync(ct);
|
LinkSnapshot snapshot = await links.SnapshotAsync(ct);
|
||||||
@@ -42,8 +52,8 @@ public static class LinkEndpoints
|
|||||||
(AgentView Dialer, AgentView Listener)? pair =
|
(AgentView Dialer, AgentView Listener)? pair =
|
||||||
snapshot.IsLinked(nodes[i].Agent, nodes[j].Agent) ? (nodes[i], nodes[j])
|
snapshot.IsLinked(nodes[i].Agent, nodes[j].Agent) ? (nodes[i], nodes[j])
|
||||||
: snapshot.IsLinked(nodes[j].Agent, nodes[i].Agent) ? (nodes[j], nodes[i])
|
: snapshot.IsLinked(nodes[j].Agent, nodes[i].Agent) ? (nodes[j], nodes[i])
|
||||||
: LinkSnapshot.IsDefaultDialer(nodes[i].Agent, nodes[j].Agent) ? (nodes[i], nodes[j])
|
: snapshot.IsDefaultDialer(nodes[i].Agent, nodes[j].Agent) ? (nodes[i], nodes[j])
|
||||||
: LinkSnapshot.IsDefaultDialer(nodes[j].Agent, nodes[i].Agent) ? (nodes[j], nodes[i])
|
: snapshot.IsDefaultDialer(nodes[j].Agent, nodes[i].Agent) ? (nodes[j], nodes[i])
|
||||||
: null;
|
: null;
|
||||||
if (pair is { } p)
|
if (pair is { } p)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ public sealed partial class AgentService
|
|||||||
_gateway = gateway;
|
_gateway = gateway;
|
||||||
_planner = planner;
|
_planner = planner;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
// 线路的默认方向看国家(国内的节点默认只拨出):国家查到或变了就重新下发
|
||||||
|
_nodes.OnCountryChanged = () => AfterAgentSetChangedAsync(CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<IReadOnlyList<AgentView>> ListAsync(CancellationToken ct)
|
public async ValueTask<IReadOnlyList<AgentView>> ListAsync(CancellationToken ct)
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ namespace MoShiro.Backend.Application;
|
|||||||
public sealed class LinkSnapshot
|
public sealed class LinkSnapshot
|
||||||
{
|
{
|
||||||
private readonly Dictionary<(string Dialer, string Listener), bool> _overrides;
|
private readonly Dictionary<(string Dialer, string Listener), bool> _overrides;
|
||||||
|
private readonly HashSet<string> _domestic;
|
||||||
|
|
||||||
public LinkSnapshot(bool auto, Dictionary<(string Dialer, string Listener), bool> overrides)
|
public LinkSnapshot(bool auto, Dictionary<(string Dialer, string Listener), bool> overrides, HashSet<string>? domestic = null)
|
||||||
{
|
{
|
||||||
Auto = auto;
|
Auto = auto;
|
||||||
_overrides = overrides;
|
_overrides = overrides;
|
||||||
|
_domestic = domestic ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>自动连接:开着时,每一对节点自动连成一条线路;关着时,只有手动开启的才连。</summary>
|
/// <summary>自动连接:开着时,每一对节点自动连成一条线路;关着时,只有手动开启的才连。</summary>
|
||||||
@@ -21,11 +23,25 @@ public sealed class LinkSnapshot
|
|||||||
public bool? PairOverride(string dialerId, string listenerId) => OverrideOf(dialerId, listenerId) ?? OverrideOf(listenerId, dialerId);
|
public bool? PairOverride(string dialerId, string listenerId) => OverrideOf(dialerId, listenerId) ?? OverrideOf(listenerId, dialerId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 自动连接时这一对由谁拨:只有一方监听,就由不监听的一方拨;两方都监听,由 id 较小的一方拨。
|
/// 谁更适合当被连接的一方(越大越适合):不监听的是 0;监听的是 2,但国内(国家 CN)的监听节点是 1——国内节点默认只拨出,
|
||||||
/// 一对节点只有一条线路、一个连接、一份 RTT;上下行都在这条连接上测,不再各拨一次。
|
/// 除非对端没有更好的选择。手动指定的方向不受这个影响。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsDefaultDialer(AgentRecord dialer, AgentRecord listener) =>
|
private int ListenerRank(AgentRecord node) => !node.Listens ? 0 : _domestic.Contains(node.Id.Value) ? 1 : 2;
|
||||||
listener.Listens && (!dialer.Listens || string.CompareOrdinal(dialer.Id.Value, listener.Id.Value) < 0);
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动连接时这一对由谁拨:更适合当被连接方的一方被拨;一样适合时 id 较小的一方拨。
|
||||||
|
/// 所以不监听的拨监听的,国内的拨国外的,其余按 id。一对节点只有一条线路、一个连接、一份 RTT;上下行都在这条连接上测。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 这一对最终连不连、是不是由这个方向拨:手动设置优先(<c>true</c> 的方向就是拨号方,两个方向都设了 <c>true</c> 时按默认方向),
|
/// 这一对最终连不连、是不是由这个方向拨:手动设置优先(<c>true</c> 的方向就是拨号方,两个方向都设了 <c>true</c> 时按默认方向),
|
||||||
@@ -60,7 +76,7 @@ public sealed class LinkSnapshot
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 哪些(拨号方,监听方)要连成线路。由面板决定、拨号方主动拨出:每一对节点只有一条线路,自动连接开着时都连(谁拨见 <see cref="LinkSnapshot.IsDefaultDialer"/>),
|
/// 哪些(拨号方,监听方)要连成线路。由面板决定、拨号方主动拨出:每一对节点只有一条线路,自动连接开着时都连(谁拨见 <see cref="LinkSnapshot.IsDefaultDialer"/>,国内节点默认只拨出),
|
||||||
/// 关着时只连手动选中的;每一对还可以手动强制开或关,并且用 <c>true</c> 选定由哪一方拨。手动设置存在设置表里(<c>link.<拨号方>.<监听方></c>,值 1 或 0,空表示没设置),一对节点最多保留一条。
|
/// 关着时只连手动选中的;每一对还可以手动强制开或关,并且用 <c>true</c> 选定由哪一方拨。手动设置存在设置表里(<c>link.<拨号方>.<监听方></c>,值 1 或 0,空表示没设置),一对节点最多保留一条。
|
||||||
/// 判断结果分别用在拨号方的拨号目标、监听方允许的拨号方(<c>peers</c>)和线路目录里;改动之后要调用 <see cref="AgentService.AfterAgentSetChangedAsync"/> 重新下发。
|
/// 判断结果分别用在拨号方的拨号目标、监听方允许的拨号方(<c>peers</c>)和线路目录里;改动之后要调用 <see cref="AgentService.AfterAgentSetChangedAsync"/> 重新下发。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -69,10 +85,12 @@ public sealed class LinkService
|
|||||||
private const string Prefix = "link.";
|
private const string Prefix = "link.";
|
||||||
|
|
||||||
private readonly ISettingsStore _settings;
|
private readonly ISettingsStore _settings;
|
||||||
|
private readonly NodeInfoService _nodes;
|
||||||
|
|
||||||
public LinkService(ISettingsStore settings)
|
public LinkService(ISettingsStore settings, NodeInfoService nodes)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
_nodes = nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<LinkSnapshot> SnapshotAsync(CancellationToken ct)
|
public async ValueTask<LinkSnapshot> SnapshotAsync(CancellationToken ct)
|
||||||
@@ -88,7 +106,8 @@ public sealed class LinkService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LinkSnapshot(all.GetValueOrDefault(SettingsService.AutoLink) != "false", overrides);
|
HashSet<string> domestic = [.. (await _nodes.ListAsync(ct)).Where(n => n.Country == "CN").Select(n => n.NodeId)];
|
||||||
|
return new LinkSnapshot(all.GetValueOrDefault(SettingsService.AutoLink) != "false", overrides, domestic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>手动设置一对线路:<c>true</c> 强制连并由 <paramref name="dialerId"/> 拨、<c>false</c> 强制不连、<c>null</c> 取消手动设置。反方向的旧设置同时清掉。</summary>
|
/// <summary>手动设置一对线路:<c>true</c> 强制连并由 <paramref name="dialerId"/> 拨、<c>false</c> 强制不连、<c>null</c> 取消手动设置。反方向的旧设置同时清掉。</summary>
|
||||||
@@ -98,6 +117,17 @@ public sealed class LinkService
|
|||||||
await _settings.SetAsync($"{Prefix}{dialerId}.{listenerId}", enabled is null ? "" : enabled.Value ? "1" : "0", ct);
|
await _settings.SetAsync($"{Prefix}{dialerId}.{listenerId}", enabled is null ? "" : enabled.Value ? "1" : "0", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>恢复默认连接模式:清掉所有手动设置并打开自动连接,一对节点谁拨谁完全按默认规则。</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>节点被删除:清掉所有涉及它的手动设置。</summary>
|
/// <summary>节点被删除:清掉所有涉及它的手动设置。</summary>
|
||||||
public async ValueTask ForgetNodeAsync(string nodeId, CancellationToken ct)
|
public async ValueTask ForgetNodeAsync(string nodeId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ public sealed class NodeInfoService
|
|||||||
private readonly ConcurrentDictionary<string, long> _lastTry = new();
|
private readonly ConcurrentDictionary<string, long> _lastTry = new();
|
||||||
private Task? _load;
|
private Task? _load;
|
||||||
|
|
||||||
|
/// <summary>某个节点的国家变了(第一次查到或查到了不同的结果)时调用:线路的默认方向取决于国家,需要重新下发。</summary>
|
||||||
|
public Func<ValueTask>? OnCountryChanged { get; set; }
|
||||||
|
|
||||||
public NodeInfoService(INodeMetaStore store, IGeoLookup geo, TimeProvider time)
|
public NodeInfoService(INodeMetaStore store, IGeoLookup geo, TimeProvider time)
|
||||||
{
|
{
|
||||||
_store = store;
|
_store = store;
|
||||||
@@ -148,16 +151,23 @@ public sealed class NodeInfoService
|
|||||||
}
|
}
|
||||||
|
|
||||||
string country = geo.Country.ToUpperInvariant();
|
string country = geo.Country.ToUpperInvariant();
|
||||||
|
bool countryChanged = !string.Equals(current.Info.Country, country, StringComparison.Ordinal);
|
||||||
if (current.Info.IspManual)
|
if (current.Info.IspManual)
|
||||||
{
|
{
|
||||||
// 运营商是手动指定的:只更新国家,不覆盖运营商
|
// 运营商是手动指定的:只更新国家,不覆盖运营商
|
||||||
await SaveAsync(current with { Info = current.Info with { Country = country }, GeoHost = host, GeoAtMs = now, UpdatedAtMs = now }, ct);
|
await SaveAsync(current with { Info = current.Info with { Country = country }, GeoHost = host, GeoAtMs = now, UpdatedAtMs = now }, ct);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
(string key, string name) = IspClassifier.Classify(geo.Asn, geo.Organization);
|
(string key, string name) = IspClassifier.Classify(geo.Asn, geo.Organization);
|
||||||
await SaveAsync(current with { Info = current.Info with { Country = country, IspKey = key, IspName = Clean(name, 40) }, GeoHost = host, GeoAtMs = now, UpdatedAtMs = now }, ct);
|
await SaveAsync(current with { Info = current.Info with { Country = country, IspKey = key, IspName = Clean(name, 40) }, GeoHost = host, GeoAtMs = now, UpdatedAtMs = now }, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (countryChanged && OnCountryChanged is { } notify)
|
||||||
|
{
|
||||||
|
await notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_inflight.TryRemove(nodeId, out _);
|
_inflight.TryRemove(nodeId, out _);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
using Fleet.Server;
|
||||||
|
using MoShiro.Backend.Application;
|
||||||
using MoShiro.TestSupport;
|
using MoShiro.TestSupport;
|
||||||
|
|
||||||
namespace MoShiro.Backend.Tests;
|
namespace MoShiro.Backend.Tests;
|
||||||
@@ -60,6 +62,56 @@ public sealed class LinkTests : IClassFixture<BackendFixture>
|
|||||||
Assert.True((await LinkAsync(a, b))!["linked"]!.GetValue<bool>());
|
Assert.True((await LinkAsync(a, b))!["linked"]!.GetValue<bool>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static AgentRecord Node(string id, bool listens) =>
|
||||||
|
new(new Fleet.Core.NodeId(id), id, NodeStatus.Active, [], [], "", 0, [], [], true, [], null, null, 0, 0, null, 0, null, 0, Listens: listens);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultDialer_NonListeningDials_ThenDomesticDialsForeign_ThenSmallerId()
|
||||||
|
{
|
||||||
|
AgentRecord home = Node("agt_a", listens: false);
|
||||||
|
AgentRecord cn = Node("agt_b", listens: true);
|
||||||
|
AgentRecord abroad = Node("agt_c", listens: true);
|
||||||
|
AgentRecord abroad2 = Node("agt_d", listens: true);
|
||||||
|
var snapshot = new LinkSnapshot(true, [], ["agt_b"]);
|
||||||
|
|
||||||
|
Assert.True(snapshot.IsLinked(home, cn)); // 不监听的拨监听的
|
||||||
|
Assert.False(snapshot.IsLinked(cn, home));
|
||||||
|
Assert.True(snapshot.IsLinked(cn, abroad)); // 国内的拨国外的,哪怕它的 id 更小
|
||||||
|
Assert.False(snapshot.IsLinked(abroad, cn));
|
||||||
|
Assert.True(snapshot.IsLinked(abroad, abroad2)); // 一样的按 id,小的拨
|
||||||
|
Assert.False(snapshot.IsLinked(abroad2, abroad));
|
||||||
|
Assert.True(snapshot.IsLinked(home, cn)); // 只有国内的在监听时,国内的也得当监听方
|
||||||
|
|
||||||
|
// 手动指定的方向不受国家影响:让国外的拨国内的
|
||||||
|
var manual = new LinkSnapshot(true, new() { [("agt_c", "agt_b")] = true }, ["agt_b"]);
|
||||||
|
Assert.True(manual.IsLinked(abroad, cn));
|
||||||
|
Assert.False(manual.IsLinked(cn, abroad));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Links_ResetDropsManualSettingsAndTurnsAutomaticLinkingOn()
|
||||||
|
{
|
||||||
|
await using AgentSimulator first = await _backend.ConnectAgentAsync();
|
||||||
|
await using AgentSimulator second = await _backend.ConnectAgentAsync();
|
||||||
|
string[] ids = [.. new[] { first.NodeId.Value, second.NodeId.Value }.Order(StringComparer.Ordinal)];
|
||||||
|
await SetLinkAsync(ids[1], ids[0], true); // 手动让大 id 的拨小 id 的
|
||||||
|
await SetAutoLinkAsync(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using HttpResponseMessage reset = await _backend.Admin.PostAsync("/api/admin/links/reset", BackendFixture.Json(new JsonObject()));
|
||||||
|
reset.EnsureSuccessStatusCode();
|
||||||
|
JsonNode matrix = JsonNode.Parse(await reset.Content.ReadAsStringAsync())!;
|
||||||
|
Assert.True(matrix["auto_link"]!.GetValue<bool>());
|
||||||
|
JsonNode link = (await LinkAsync(ids[0], ids[1]))!; // 回到默认方向:小 id 拨大 id
|
||||||
|
Assert.True(link["linked"]!.GetValue<bool>());
|
||||||
|
Assert.Null(link["override"]);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await SetAutoLinkAsync(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Links_FollowTheAutomaticSwitch_AndManualChoicesAlwaysWin()
|
public async Task Links_FollowTheAutomaticSwitch_AndManualChoicesAlwaysWin()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Chip, Empty, ErrorNotice, Spinner, useAction } from '@shared/ui'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 线路:Agent 之间的线路,一对 Agent 一条。自动连接开着时所有可达的 Agent 两两相连,关着时只连手动选中的;
|
* 线路:Agent 之间的线路,一对 Agent 一条。自动连接开着时所有可达的 Agent 两两相连,关着时只连手动选中的;
|
||||||
* 每一对都可以手动强制开或关(点一下切换,「↺」回到自动)。拨号的一方由面板决定(不监听的拨监听的;都监听时 id 较小的拨)。
|
* 每一对都可以手动强制开或关(点一下切换,「↺」回到自动)。拨号的一方由面板决定(不监听的拨监听的;国内的节点默认只拨出;其余 id 较小的拨),「恢复默认连接模式」清掉所有手动设置。
|
||||||
*/
|
*/
|
||||||
/** 线路连接状态:control_* 是与 Agent 的控制连接(断开即失联),task_* 是测量用的 QUIC 数据通道。 */
|
/** 线路连接状态:control_* 是与 Agent 的控制连接(断开即失联),task_* 是测量用的 QUIC 数据通道。 */
|
||||||
const CONNECTION_LABELS: Record<string, string> = {
|
const CONNECTION_LABELS: Record<string, string> = {
|
||||||
@@ -36,6 +36,11 @@ export function Links({ dialerId }: { dialerId?: string }) {
|
|||||||
await run(() => api.post(`/api/admin/links/${link.dialer_id}/${link.listener_id}/retry`), '已要求 Agent 立即重试;若 Agent 离线,恢复后会自动重连')
|
await run(() => api.post(`/api/admin/links/${link.dialer_id}/${link.listener_id}/retry`), '已要求 Agent 立即重试;若 Agent 离线,恢复后会自动重连')
|
||||||
matrix.reload()
|
matrix.reload()
|
||||||
}
|
}
|
||||||
|
const resetLinks = async () => {
|
||||||
|
if (!window.confirm('清掉所有手动设置并打开自动连接,每对 Agent 按默认规则重新连线?')) return
|
||||||
|
await run(() => api.post('/api/admin/links/reset'), '已恢复默认连接模式')
|
||||||
|
matrix.reload()
|
||||||
|
}
|
||||||
const setAuto = async (on: boolean) => {
|
const setAuto = async (on: boolean) => {
|
||||||
await run(() => api.patch('/api/admin/settings', { 'fleet.auto_link': on }), on ? '已开启自动连接' : '已关闭自动连接')
|
await run(() => api.patch('/api/admin/settings', { 'fleet.auto_link': on }), on ? '已开启自动连接' : '已关闭自动连接')
|
||||||
matrix.reload()
|
matrix.reload()
|
||||||
@@ -47,7 +52,8 @@ export function Links({ dialerId }: { dialerId?: string }) {
|
|||||||
<b>{dialerId ? '拨出连接' : 'Agent 节点画板'}</b>
|
<b>{dialerId ? '拨出连接' : 'Agent 节点画板'}</b>
|
||||||
{matrix.loading && !matrix.data ? <Spinner /> : null}
|
{matrix.loading && !matrix.data ? <Spinner /> : null}
|
||||||
<span className="muted">{dialerId ? '点格子调整单方向的连接策略。' : '每对 Agent 一条线路,只测一次 RTT;点击连线查看质量或开关。'}</span>
|
<span className="muted">{dialerId ? '点格子调整单方向的连接策略。' : '每对 Agent 一条线路,只测一次 RTT;点击连线查看质量或开关。'}</span>
|
||||||
{!dialerId ? <label className="switch right">
|
{!dialerId ? <button type="button" className="btn small right" disabled={busy} title="默认规则:不监听的节点拨监听的;国内(CN)的节点默认只拨出、不当被连接方;其余按 id 小的拨。手动设置会被清掉。" onClick={resetLinks}>恢复默认连接模式</button> : null}
|
||||||
|
{!dialerId ? <label className="switch">
|
||||||
<input type="checkbox" checked={matrix.data?.auto_link ?? true} disabled={busy} onChange={(e) => setAuto(e.target.checked)} />
|
<input type="checkbox" checked={matrix.data?.auto_link ?? true} disabled={busy} onChange={(e) => setAuto(e.target.checked)} />
|
||||||
自动连接 {matrix.data?.auto_link === false ? '关' : '开'}
|
自动连接 {matrix.data?.auto_link === false ? '关' : '开'}
|
||||||
</label> : null}
|
</label> : null}
|
||||||
|
|||||||
@@ -1040,6 +1040,23 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/admin/links/reset": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/** Drop every manual link setting and turn automatic linking on: who dials whom follows the default rules again */
|
||||||
|
post: operations["links_reset"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/admin/links/{dialer_id}/{listener_id}": {
|
"/api/admin/links/{dialer_id}/{listener_id}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -5891,6 +5908,44 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
links_reset: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["LinkMatrixDto"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden */
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
links_set: {
|
links_set: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
Reference in new issue
Block a user