Files
MoShiro/backend/shared/MoShiro.Protocol/Rpc/MoShiroRpc.cs
T

837 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
using System.Text.Json.Serialization;
using Fleet.Core;
namespace MoShiro.Protocol;
/// <summary>moshiro.* 方法名。新增方法 = 在这里定义名称和 params/result,再在组装处加一行 <c>.On(...)</c>。</summary>
public static class MoShiroMethods
{
public const string TestRun = "moshiro.test.run";
public const string TestCancel = "moshiro.test.cancel";
public const string DialerDisconnect = "moshiro.dialer.disconnect";
public const string LinkRetry = "moshiro.link.retry";
public const string TestStatus = "moshiro.test.status";
public const string Event = "moshiro.event";
public const string Host = "moshiro.host";
public const string UpdateOffer = "moshiro.update.offer";
public const string UpdateApply = "moshiro.update.apply";
public const string Schedule = "moshiro.schedule";
public const string LogsFetch = "moshiro.logs.fetch";
// 拨号方 ↔ 监听方(点对点会话)
public const string Clock = "moshiro.clock";
public const string TestPhase = "moshiro.test.phase";
public const string TestPhaseEnd = "moshiro.test.phase_end";
public const string TestAbort = "moshiro.test.abort";
}
public static class AgentEventKinds
{
public const string DialerConnected = "dialer_connected";
public const string DialerDisconnected = "dialer_disconnected";
public const string EndpointChanged = "endpoint_changed";
public const string SessionRejected = "session_rejected";
public const string ClockStep = "clock_step";
public const string Gap = "gap";
public const string CpuSaturated = "cpu_saturated";
public static readonly IReadOnlyList<string> All =
[DialerConnected, DialerDisconnected, EndpointChanged, SessionRejected, ClockStep, Gap, CpuSaturated];
}
public static class TestStatuses
{
public const string Queued = "queued";
public const string Running = "running";
public const string PhaseDone = "phase_done";
public const string Succeeded = "succeeded";
public const string Failed = "failed";
public const string Aborted = "aborted";
}
public sealed class TestPhase
{
[JsonPropertyName("kind")]
public string Kind { get; set; } = "";
[JsonPropertyName("params")]
public Dictionary<string, JsonElement> Params { get; set; } = [];
[JsonPropertyName("duration_s")]
public int DurationSeconds { get; set; }
}
public sealed class TestRunParams
{
[JsonPropertyName("ip_version")]
public string IpVersion { get; set; } = IpVersions.V4;
[JsonPropertyName("test_id")]
public TestId TestId { get; set; }
/// <summary>在哪个拨号方上运行:必须是当前连着这个监听方的节点。</summary>
[JsonPropertyName("dialer_id")]
public NodeId DialerId { get; set; }
[JsonPropertyName("plan")]
public string Plan { get; set; } = "";
[JsonPropertyName("phases")]
public List<TestPhase> Phases { get; set; } = [];
}
public sealed class TestRunResult
{
[JsonPropertyName("accepted")]
public bool Accepted { get; set; }
}
public sealed class TestCancelParams
{
[JsonPropertyName("test_id")]
public TestId TestId { get; set; }
}
/// <summary>Backend → 监听方:断开某个拨号方的会话(拨号方按退避重连,除非它已经被吊销)。</summary>
public sealed class DialerDisconnectParams
{
[JsonPropertyName("dialer_id")]
public NodeId DialerId { get; set; }
}
/// <summary>Backend → 拨号方:立即重试到指定监听方的线路。</summary>
public sealed class LinkRetryParams
{
[JsonPropertyName("listener_id")]
public NodeId ListenerId { get; set; }
}
public sealed class TestStatusParams
{
[JsonPropertyName("test_id")]
public TestId TestId { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; } = "";
[JsonPropertyName("phase")]
public int? Phase { get; set; }
[JsonPropertyName("error_code")]
public string? ErrorCode { get; set; }
}
public sealed class AgentEventParams
{
/// <summary><c>dialer_connected</c> 时:这个拨号方的 id、版本、地址和连接时间(Backend 不用等下一次心跳就能显示)。</summary>
[JsonPropertyName("dialer")]
public ConnectedDialer? Dialer { get; set; }
[JsonPropertyName("kind")]
public string Kind { get; set; } = "";
[JsonPropertyName("at_utc_ms")]
public long AtUtcMs { get; set; }
[JsonPropertyName("detail")]
public string? Detail { get; set; }
}
/// <summary>拨号目标:一个监听节点的地址、端口和固定证书用的公钥。</summary>
public sealed class DialTarget
{
[JsonPropertyName("node_id")]
public NodeId NodeId { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("host")]
public string Host { get; set; } = "";
[JsonPropertyName("ipv6_host")]
public string? Ipv6Host { get; set; }
[JsonPropertyName("tcp_port")]
public int TcpPort { get; set; }
[JsonPropertyName("quic_ports")]
public List<int> QuicPorts { get; set; } = [];
/// <summary>监听方面向拨号方的 TLS 证书公钥(SPKI,base64):证书按它的 SHA-256 指纹固定,敲门密钥由它和拨号方的身份密钥派生。</summary>
[JsonPropertyName("public_key")]
public string PublicKey { get; set; } = "";
}
/// <summary>期望状态文档(Fleet state type = <c>dial_targets</c>):Backend 下发给每个节点的、它要拨出的监听节点列表。</summary>
public sealed class DialTargets
{
public const string StateType = "dial_targets";
[JsonPropertyName("version")]
public long Version { get; set; }
[JsonPropertyName("targets")]
public List<DialTarget> Targets { get; set; } = [];
[JsonPropertyName("trust_chain")]
public List<SignedStatementDto>? TrustChain { get; set; }
}
/// <summary>注册元数据(MoShiro 定义,经 Fleet 的 <c>meta</c> 字段带上来)。不监听的节点不带端口。</summary>
public sealed class NodeEnrollMeta
{
[JsonPropertyName("tcp_port")]
public int TcpPort { get; set; }
[JsonPropertyName("quic_ports")]
public List<int> QuicPorts { get; set; } = [];
[JsonPropertyName("public_host")]
public string? PublicHost { get; set; }
[JsonPropertyName("kinds")]
public List<string> Kinds { get; set; } = [];
}
public sealed class LocalLimits
{
[JsonPropertyName("max_total_mbps")]
public double MaxTotalMbps { get; set; }
[JsonPropertyName("max_total_pps")]
public long MaxTotalPps { get; set; }
[JsonPropertyName("allowed_target_cidrs")]
public List<string> AllowedTargetCidrs { get; set; } = [];
[JsonPropertyName("max_concurrent_tests")]
public int MaxConcurrentTests { get; set; } = 1;
}
/// <summary>
/// 节点的系统和架构(粗粒度,只用于展示):<c>os</c> 是 linux / windows / macos / other,<c>name</c> 是「Debian 12」「Windows 11」这样的名字,
/// <c>arch</c> 是 x64 / arm64 / arm / x86。<b>没有主机名、内核版本、具体型号</b>。另外带着这个程序自己的构建信息(版本、tag、构建时间)。
/// </summary>
public sealed class PlatformInfo
{
[JsonPropertyName("os")]
public string Os { get; set; } = "other";
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("arch")]
public string? Arch { get; set; }
/// <summary>程序的版本、tag、提交(短)和构建时间(UTC,ISO 8601):<see cref="BuildInfo"/> 构建时写进去的,没设置就是空。</summary>
[JsonPropertyName("version")]
public string? Version { get; set; }
[JsonPropertyName("tag")]
public string? Tag { get; set; }
[JsonPropertyName("commit")]
public string? Commit { get; set; }
[JsonPropertyName("built_at")]
public string? BuiltAt { get; set; }
}
/// <summary>
/// 面板给节点的更新清单(<c>moshiro.update.offer</c>):<c>manifest</c> 是发布清单的原始文本,<c>signature</c> 是 base64 的签名。
/// 节点只是把它们原样放进数据目录;验签、拉取、重启都由宿主机上的更新器完成(节点进程不执行任何命令,也不信任清单的内容)。
/// </summary>
public sealed class UpdateOfferParams
{
[JsonPropertyName("manifest")]
public string Manifest { get; set; } = "";
[JsonPropertyName("signature")]
public string Signature { get; set; } = "";
}
/// <summary>
/// 面板给节点的定时重启设置(<c>moshiro.schedule</c> 通知):<c>restart_at</c> 形如 <c>04:30+08:00</c>,为空表示不定时重启。
/// 节点连上时补发、设置变化时群发;节点本地写了 <c>restart_at</c> 或环境变量 <c>MOSHIRO_RESTART_AT</c> 时以本地为准。
/// 节点只是到点自己有序退出,由服务管理器拉起,不执行任何命令。
/// </summary>
public sealed class ScheduleParams
{
[JsonPropertyName("restart_at")]
public string? RestartAt { get; set; }
}
/// <summary>宿主机更新器已经验证过的可用更新。</summary>
public sealed class UpdateAvailable
{
[JsonPropertyName("version")]
public string? Version { get; set; }
[JsonPropertyName("tag")]
public string? Tag { get; set; }
[JsonPropertyName("digest")]
public string? Digest { get; set; }
[JsonPropertyName("seq")]
public long Seq { get; set; }
}
/// <summary>
/// 宿主机更新器的状态(它写在 <c>update-status/status.json</c>,节点读出来随心跳上报):
/// <c>up_to_date</c>、<c>available</c>、<c>applying</c>、<c>failed</c>、<c>rolled_back</c>、<c>invalid</c>(清单验签或校验没过)。
/// 没有安装更新器的节点没有这个字段。
/// </summary>
public sealed class UpdateStatus
{
[JsonPropertyName("state")]
public string State { get; set; } = "";
/// <summary>正在运行的镜像 digest(更新器记录的;还没更新过时为空)。</summary>
[JsonPropertyName("current")]
public string? Current { get; set; }
[JsonPropertyName("available")]
public UpdateAvailable? Available { get; set; }
[JsonPropertyName("error")]
public string? Error { get; set; }
[JsonPropertyName("at")]
public long At { get; set; }
}
/// <summary>节点的 <c>fleet.heartbeat</c> 附加字段:进程内的计数、拨号侧的线路健康和监听侧的会话快照,没有主机信息。</summary>
public sealed class NodeHeartbeatExtra
{
/// <summary>Node operating state; registration approval and transport presence are reported separately.</summary>
[JsonPropertyName("runtime_status")]
public NodeRuntimeStatus RuntimeStatus { get; set; } = NodeRuntimeStatus.Running;
/// <summary>拨号侧:到每个监听节点(每个 IP 族)的线路健康。</summary>
[JsonPropertyName("links")]
public List<LinkHealth>? Links { get; set; }
[JsonPropertyName("ipv6")]
public Ipv6Diagnostic? Ipv6 { get; set; }
/// <summary>监听侧:当前连着的拨号方,每次心跳都是完整的快照,Backend 靠它纠正错过的 dialer_connected / dialer_disconnected 事件。</summary>
[JsonPropertyName("dialers")]
public List<ConnectedDialer> Dialers { get; set; } = [];
[JsonPropertyName("pps_in")]
public long PpsIn { get; set; }
[JsonPropertyName("pps_out")]
public long PpsOut { get; set; }
[JsonPropertyName("dropped_invalid")]
public long DroppedInvalid { get; set; }
[JsonPropertyName("dropped_rate_limited")]
public long DroppedRateLimited { get; set; }
[JsonPropertyName("queue_bytes")]
public long QueueBytes { get; set; }
[JsonPropertyName("update")]
public UpdateStatus? Update { get; set; }
/// <summary>日志只报计数(不带文本):自上次心跳以来各级别的条数、最多几个出现最多的类型,以及最新一条的编号;文本由 Backend 用 <c>moshiro.logs.fetch</c> 按需拉取。</summary>
[JsonPropertyName("log_summary")]
public LogSummaryDto? LogSummary { get; set; }
}
/// <summary>某一类日志(消息文本的稳定哈希)在一个级别上出现了多少次。</summary>
public sealed class LogTypeCountDto
{
[JsonPropertyName("type")]
public uint Type { get; set; }
[JsonPropertyName("level")]
public string Level { get; set; } = "info";
[JsonPropertyName("count")]
public long Count { get; set; }
}
public sealed class LogSummaryDto
{
[JsonPropertyName("last_id")]
public long LastId { get; set; }
[JsonPropertyName("info")]
public long Info { get; set; }
[JsonPropertyName("warn")]
public long Warn { get; set; }
[JsonPropertyName("error")]
public long Error { get; set; }
[JsonPropertyName("types")]
public List<LogTypeCountDto> Types { get; set; } = [];
}
/// <summary>Backend → 节点:拉取编号大于 <c>after_id</c> 的日志(最旧的在前),级别不低于 <c>min_level</c>。</summary>
public sealed class LogsFetchParams
{
[JsonPropertyName("after_id")]
public long AfterId { get; set; }
[JsonPropertyName("limit")]
public int Limit { get; set; } = 100;
[JsonPropertyName("min_level")]
public string? MinLevel { get; set; }
}
public sealed class LogEntryDto
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("type")]
public uint Type { get; set; }
[JsonPropertyName("at_utc_ms")]
public long AtUtcMs { get; set; }
[JsonPropertyName("level")]
public string Level { get; set; } = "info";
[JsonPropertyName("message")]
public string Message { get; set; } = "";
[JsonPropertyName("fields")]
public Dictionary<string, string> Fields { get; set; } = [];
}
public sealed class LogsFetchResult
{
[JsonPropertyName("items")]
public List<LogEntryDto> Items { get; set; } = [];
/// <summary>节点上最新一条的编号:比 <c>after_id</c> 小说明节点重启过,编号从头开始。</summary>
[JsonPropertyName("last_id")]
public long LastId { get; set; }
}
/// <summary>
/// 拨号方到一个监听方(一个 IP 族)的线路健康。控制层(TCP+TLS 长连接,承载控制 RPC 和回传数据)与任务层(测量用的 TCP / QUIC)
/// 各有一个状态机:控制层断开才算两个节点失联,任务层只影响测量。
/// </summary>
public sealed class LinkHealth
{
[JsonPropertyName("listener_id")]
public string ListenerId { get; set; } = "";
[JsonPropertyName("ip_version")]
public string IpVersion { get; set; } = IpVersions.V4;
[JsonPropertyName("control")]
public ControlHealth Control { get; set; } = new();
[JsonPropertyName("task")]
public TaskHealth Task { get; set; } = new();
/// <summary>控制层在线且 QUIC 数据通道就绪:这条线路可以完整测量。</summary>
[JsonIgnore]
public bool Ready => Control.State == ControlState.Up && Task.Quic.State == QuicState.Ready;
}
/// <summary>某一层最近一次失败的原因:失败发生的阶段、分类和说明。</summary>
public sealed class LinkFault
{
[JsonPropertyName("phase")]
public string Phase { get; set; } = "";
/// <summary><c>timeout</c>、<c>refused</c>、<c>reset</c>、<c>dns</c>、<c>tls</c>、<c>auth</c>、<c>closed</c>、<c>silent</c>、<c>other</c>。</summary>
[JsonPropertyName("kind")]
public string Kind { get; set; } = "other";
[JsonPropertyName("message")]
public string? Message { get; set; }
public override string ToString() => string.IsNullOrEmpty(Message) ? $"{Phase}: {Kind}" : $"{Phase}: {Kind}: {Message}";
}
/// <summary>控制层:到 Agent 的 TCP+TLS 长连接,包括认证、时钟同步,以及回传数据的发件箱。</summary>
public sealed class ControlHealth
{
[JsonPropertyName("state")]
public ControlState State { get; set; } = ControlState.Dialing;
/// <summary>进入当前状态的时间(Unix 毫秒)。</summary>
[JsonPropertyName("since_ms")]
public long SinceMs { get; set; }
[JsonPropertyName("last_error")]
public LinkFault? LastError { get; set; }
[JsonPropertyName("retry_attempt")]
public int RetryAttempt { get; set; }
[JsonPropertyName("retry_in_ms")]
public long? RetryInMs { get; set; }
/// <summary>发件箱里还没有被确认的回传数据。</summary>
[JsonPropertyName("outbox_bytes")]
public long OutboxBytes { get; set; }
[JsonPropertyName("outbox_pending")]
public int OutboxPending { get; set; }
}
/// <summary>任务层:测量自己使用的链路。TCP 类积木自带套接字和重拨,QUIC 数据通道由拨号方单独维护。</summary>
public sealed class TaskHealth
{
/// <summary>正在运行的 TCP 类线路积木数(tcp_ping、connect、hold、tcp_transfer)。</summary>
[JsonPropertyName("tcp_blocks")]
public int TcpBlocks { get; set; }
[JsonPropertyName("quic")]
public QuicHealth Quic { get; set; } = new();
}
/// <summary>QUIC 数据通道(quic_ping、quic_stream 使用)的状态、握手统计和传输层统计。</summary>
public sealed class QuicHealth
{
[JsonPropertyName("state")]
public QuicState State { get; set; } = QuicState.Idle;
[JsonPropertyName("since_ms")]
public long SinceMs { get; set; }
[JsonPropertyName("last_error")]
public LinkFault? LastError { get; set; }
[JsonPropertyName("retry_attempt")]
public int RetryAttempt { get; set; }
[JsonPropertyName("retry_in_ms")]
public long? RetryInMs { get; set; }
[JsonPropertyName("handshake_attempts")]
public long HandshakeAttempts { get; set; }
[JsonPropertyName("handshake_successes")]
public long HandshakeSuccesses { get; set; }
[JsonPropertyName("handshake_failures")]
public long HandshakeFailures { get; set; }
[JsonPropertyName("handshake_timeouts")]
public long HandshakeTimeouts { get; set; }
[JsonPropertyName("handshake_p95_ms")]
public double? HandshakeP95Ms { get; set; }
[JsonPropertyName("handshake_sample_count")]
public long HandshakeSampleCount { get; set; }
/// <summary>QUIC packet-loss estimate from MsQuic's non-spurious loss counters; not Datagram application loss or retransmission.</summary>
[JsonPropertyName("transport_packet_loss_rate")]
public double? TransportPacketLossRate { get; set; }
[JsonPropertyName("transport_packets_sent")]
public ulong? TransportPacketsSent { get; set; }
[JsonPropertyName("transport_congestion_events")]
public uint? TransportCongestionEvents { get; set; }
[JsonPropertyName("transport_ecn_congestion_events")]
public uint? TransportEcnCongestionEvents { get; set; }
[JsonPropertyName("path_mtu")]
public ushort? PathMtu { get; set; }
}
/// <summary>控制层状态:<c>dialing</c>(DNS、TCP、敲门、TLS)→ <c>authenticating</c>(身份签名)→ <c>syncing</c>(时钟)→ <c>up</c>;失败进入 <c>backoff</c>。</summary>
[JsonConverter(typeof(JsonStringEnumConverter<ControlState>))]
public enum ControlState
{
[JsonStringEnumMemberName("dialing")]
Dialing,
[JsonStringEnumMemberName("authenticating")]
Authenticating,
[JsonStringEnumMemberName("syncing")]
Syncing,
[JsonStringEnumMemberName("up")]
Up,
[JsonStringEnumMemberName("backoff")]
Backoff,
}
/// <summary>QUIC 数据通道状态:控制层不在线时是 <c>idle</c>;<c>connecting</c> 握手中;<c>ready</c> 可发送;<c>retrying</c> 断开后等待重拨。</summary>
[JsonConverter(typeof(JsonStringEnumConverter<QuicState>))]
public enum QuicState
{
[JsonStringEnumMemberName("idle")]
Idle,
[JsonStringEnumMemberName("connecting")]
Connecting,
[JsonStringEnumMemberName("ready")]
Ready,
[JsonStringEnumMemberName("retrying")]
Retrying,
}
/// <summary>节点的 <c>fleet.hello</c> 附加字段:属主、系统与构建信息,以及(监听时)面向拨号方的接入信息。</summary>
public sealed class NodeHelloMeta
{
/// <summary>这个节点的属主:安装时固定在节点本地的 passkey 凭证 id。面板据此知道它绑定在哪个账户的哪把 passkey 上。</summary>
[JsonPropertyName("owners")]
public List<string>? Owners { get; set; }
[JsonPropertyName("ipv6")]
public Ipv6Diagnostic? Ipv6 { get; set; }
[JsonPropertyName("platform")]
public PlatformInfo? Platform { get; set; }
[JsonPropertyName("local_limits")]
public LocalLimits? LocalLimits { get; set; }
/// <summary>监听接入信息;节点不监听时为 null,面板不会把它当作拨号目标。</summary>
[JsonPropertyName("listen")]
public ListenInfo? Listen { get; set; }
}
/// <summary>节点作为监听方时,面向拨号方的接入信息。</summary>
public sealed class ListenInfo
{
[JsonPropertyName("tcp_port")]
public int TcpPort { get; set; }
[JsonPropertyName("quic_ports")]
public List<int> QuicPorts { get; set; } = [];
/// <summary>本机是否启用了 IPv6 监听;面板只给启用了的节点安排 IPv6 会话。</summary>
[JsonPropertyName("ipv6_supported")]
public bool Ipv6Supported { get; set; }
/// <summary>本机的公网地址(IPv4 公网地址、IPv6 全球单播地址):面板据此自动填 <c>public_host</c> 和 <c>ipv6_host</c>,管理员手填的优先。</summary>
[JsonPropertyName("addresses")]
public List<string>? Addresses { get; set; }
}
/// <summary>
/// 主机资源占用的只读快照(Agent → Backend,每隔几秒一次)。只有汇总的数字:<b>没有 CPU 型号、主机名、网卡名,也没有任何 IP 地址</b>。
/// 读不到的项是 null。
/// </summary>
public sealed class HostSnapshot
{
[JsonPropertyName("cpu_percent")]
public double? CpuPercent { get; set; }
[JsonPropertyName("mem_used")]
public long? MemUsed { get; set; }
[JsonPropertyName("mem_total")]
public long? MemTotal { get; set; }
[JsonPropertyName("disk_used")]
public long? DiskUsed { get; set; }
[JsonPropertyName("disk_total")]
public long? DiskTotal { get; set; }
/// <summary>所有物理网卡的累计收发字节(自开机以来)。</summary>
[JsonPropertyName("net_rx_total")]
public long? NetRxTotal { get; set; }
[JsonPropertyName("net_tx_total")]
public long? NetTxTotal { get; set; }
[JsonPropertyName("net_rx_bps")]
public long? NetRxBps { get; set; }
[JsonPropertyName("net_tx_bps")]
public long? NetTxBps { get; set; }
/// <summary>已统计接口的内核报告链路速率之和(Mbit/s);驱动不支持或无法读取时为空,不代表互联网带宽。</summary>
[JsonPropertyName("net_link_mbps")]
public long? NetLinkMbps { get; set; }
[JsonPropertyName("uptime_s")]
public long? UptimeSeconds { get; set; }
[JsonPropertyName("at_utc_ms")]
public long AtUtcMs { get; set; }
}
/// <summary>监听方眼中的一个已连接拨号方。</summary>
public sealed class ConnectedDialer
{
[JsonPropertyName("ip_version")]
public string IpVersion { get; set; } = IpVersions.V4;
[JsonPropertyName("id")]
public NodeId Id { get; set; }
[JsonPropertyName("version")]
public string? Version { get; set; }
/// <summary>拨号方的远端地址(经过 NAT 之后的公网 IP:端口)。</summary>
[JsonPropertyName("remote")]
public string? Remote { get; set; }
[JsonPropertyName("since_utc_ms")]
public long SinceUtcMs { get; set; }
}
/// <summary>监听方下发给拨号方的一条线路任务(期望状态类型 <c>line_task_set</c> 里的一项)。</summary>
public sealed class LineTask
{
[JsonPropertyName("id")]
public TaskId Id { get; set; }
[JsonPropertyName("kind")]
public string Kind { get; set; } = "";
[JsonPropertyName("params")]
public Dictionary<string, JsonElement> Params { get; set; } = [];
/// <summary>数据面用它区分任务;Agent 在本进程生命周期内分配。</summary>
[JsonPropertyName("stream_id")]
public uint StreamId { get; set; }
[JsonPropertyName("port")]
public int Port { get; set; }
}
/// <summary>这条线路的期望状态(监听方 → 拨号方):拨号方按它调和自己一侧的角色。</summary>
public sealed class LineTaskSet
{
public const string StateType = "line_task_set";
[JsonPropertyName("version")]
public long Version { get; set; }
[JsonPropertyName("tasks")]
public List<LineTask> Tasks { get; set; } = [];
/// <summary>监听方的数据面端口(拨号方把包发到这里)。</summary>
[JsonPropertyName("quic_ports")]
public List<int> QuicPorts { get; set; } = [];
[JsonPropertyName("tcp_ports")]
public List<int> TcpPorts { get; set; } = [];
}
/// <summary><c>moshiro.clock</c>:拨号方用它估计与监听方单调时钟的偏移(对称往返假设)。</summary>
public sealed class ClockParams
{
[JsonPropertyName("dialer_mono_us")]
public long DialerMonoUs { get; set; }
}
public sealed class ClockResult
{
[JsonPropertyName("listener_mono_us")]
public long ListenerMonoUs { get; set; }
}
/// <summary>按需测试的一个阶段(监听方 → 拨号方):监听方编排时间,拨号方准备好自己一侧的角色之后应答。</summary>
public sealed class TestPhaseStartParams
{
[JsonPropertyName("test_id")]
public TestId TestId { get; set; }
[JsonPropertyName("phase")]
public int Phase { get; set; }
[JsonPropertyName("kind")]
public string Kind { get; set; } = "";
[JsonPropertyName("params")]
public Dictionary<string, JsonElement> Params { get; set; } = [];
[JsonPropertyName("stream_id")]
public uint StreamId { get; set; }
[JsonPropertyName("duration_s")]
public int DurationSeconds { get; set; }
[JsonPropertyName("port")]
public int Port { get; set; }
}
public sealed class TestPhaseStartResult
{
[JsonPropertyName("accepted")]
public bool Accepted { get; set; }
}
/// <summary>阶段结束(监听方 → 拨号方):拨号方收尾并把自己一侧的列块交给监听方。<c>sent_count</c> 是监听方一侧发出的包数(单向数据流用)。</summary>
public sealed class TestPhaseEndParams
{
[JsonPropertyName("test_id")]
public TestId TestId { get; set; }
[JsonPropertyName("phase")]
public int Phase { get; set; }
[JsonPropertyName("stream_id")]
public uint StreamId { get; set; }
[JsonPropertyName("sent_count")]
public long SentCount { get; set; }
}
/// <summary>拨号方收尾之后回复:它交给监听方的列块个数(监听方据此等待全部列块到齐再进入下一阶段)。</summary>
public sealed class TestPhaseEndResult
{
[JsonPropertyName("fragments")]
public int Fragments { get; set; }
}
public sealed class TestAbortParams
{
[JsonPropertyName("test_id")]
public TestId TestId { get; set; }
}
/// <summary>拨号方一侧的列块放在 RawChunk 头部 params 里的保留键:本块覆盖的序号范围 [from, to)(单向数据流由监听方一侧按它取行)。</summary>
public static class FragmentKeys
{
public const string SeqFrom = "_seq_from";
public const string SeqTo = "_seq_to";
}
[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(LineTaskSet))]
[JsonSerializable(typeof(ClockParams))]
[JsonSerializable(typeof(ClockResult))]
[JsonSerializable(typeof(TestPhaseStartParams))]
[JsonSerializable(typeof(TestPhaseStartResult))]
[JsonSerializable(typeof(TestPhaseEndParams))]
[JsonSerializable(typeof(TestPhaseEndResult))]
[JsonSerializable(typeof(TestAbortParams))]
[JsonSerializable(typeof(TaskSet))]
[JsonSerializable(typeof(TestRunParams))]
[JsonSerializable(typeof(TestRunResult))]
[JsonSerializable(typeof(TestCancelParams))]
[JsonSerializable(typeof(DialerDisconnectParams))]
[JsonSerializable(typeof(LinkRetryParams))]
[JsonSerializable(typeof(HostSnapshot))]
[JsonSerializable(typeof(TestStatusParams))]
[JsonSerializable(typeof(AgentEventParams))]
[JsonSerializable(typeof(DialTargets))]
[JsonSerializable(typeof(UpdateOfferParams))]
[JsonSerializable(typeof(ScheduleParams))]
[JsonSerializable(typeof(LogsFetchParams))]
[JsonSerializable(typeof(LogsFetchResult))]
[JsonSerializable(typeof(LogSummaryDto))]
[JsonSerializable(typeof(UpdateStatus))]
[JsonSerializable(typeof(NodeHeartbeatExtra))]
[JsonSerializable(typeof(NodeHelloMeta))]
[JsonSerializable(typeof(NodeEnrollMeta))]
[JsonSerializable(typeof(PlatformInfo))]
[JsonSerializable(typeof(Ipv6Diagnostic))]
[JsonSerializable(typeof(RawChunkHeader))]
public sealed partial class MoShiroJsonContext : JsonSerializerContext;