using System.Text.Json; using System.Text.Json.Serialization; using Fleet.Core; namespace MoShiro.Protocol; /// moshiro.* 方法名。新增方法 = 在这里定义名称和 params/result,再在组装处加一行 .On(...)。 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 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 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; } /// 在哪个拨号方上运行:必须是当前连着这个监听方的节点。 [JsonPropertyName("dialer_id")] public NodeId DialerId { get; set; } [JsonPropertyName("plan")] public string Plan { get; set; } = ""; [JsonPropertyName("phases")] public List 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; } } /// Backend → 监听方:断开某个拨号方的会话(拨号方按退避重连,除非它已经被吊销)。 public sealed class DialerDisconnectParams { [JsonPropertyName("dialer_id")] public NodeId DialerId { get; set; } } /// Backend → 拨号方:立即重试到指定监听方的线路。 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 { /// dialer_connected 时:这个拨号方的 id、版本、地址和连接时间(Backend 不用等下一次心跳就能显示)。 [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; } } /// 拨号目标:一个监听节点的地址、端口和固定证书用的公钥。 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 QuicPorts { get; set; } = []; /// 监听方面向拨号方的 TLS 证书公钥(SPKI,base64):证书按它的 SHA-256 指纹固定,敲门密钥由它和拨号方的身份密钥派生。 [JsonPropertyName("public_key")] public string PublicKey { get; set; } = ""; } /// 期望状态文档(Fleet state type = dial_targets):Backend 下发给每个节点的、它要拨出的监听节点列表。 public sealed class DialTargets { public const string StateType = "dial_targets"; [JsonPropertyName("version")] public long Version { get; set; } [JsonPropertyName("targets")] public List Targets { get; set; } = []; [JsonPropertyName("trust_chain")] public List? TrustChain { get; set; } } /// 注册元数据(MoShiro 定义,经 Fleet 的 meta 字段带上来)。不监听的节点不带端口。 public sealed class NodeEnrollMeta { [JsonPropertyName("tcp_port")] public int TcpPort { get; set; } [JsonPropertyName("quic_ports")] public List QuicPorts { get; set; } = []; [JsonPropertyName("public_host")] public string? PublicHost { get; set; } [JsonPropertyName("kinds")] public List 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 AllowedTargetCidrs { get; set; } = []; [JsonPropertyName("max_concurrent_tests")] public int MaxConcurrentTests { get; set; } = 1; } /// /// 节点的系统和架构(粗粒度,只用于展示):os 是 linux / windows / macos / other,name 是「Debian 12」「Windows 11」这样的名字, /// arch 是 x64 / arm64 / arm / x86。没有主机名、内核版本、具体型号。另外带着这个程序自己的构建信息(版本、tag、构建时间)。 /// 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; } /// 程序的版本、tag、提交(短)和构建时间(UTC,ISO 8601): 构建时写进去的,没设置就是空。 [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; } } /// /// 面板给节点的更新清单(moshiro.update.offer):manifest 是发布清单的原始文本,signature 是 base64 的签名。 /// 节点只是把它们原样放进数据目录;验签、拉取、重启都由宿主机上的更新器完成(节点进程不执行任何命令,也不信任清单的内容)。 /// public sealed class UpdateOfferParams { [JsonPropertyName("manifest")] public string Manifest { get; set; } = ""; [JsonPropertyName("signature")] public string Signature { get; set; } = ""; } /// /// 面板给节点的定时重启设置(moshiro.schedule 通知):restart_at 形如 04:30+08:00,为空表示不定时重启。 /// 节点连上时补发、设置变化时群发;节点本地写了 restart_at 或环境变量 MOSHIRO_RESTART_AT 时以本地为准。 /// 节点只是到点自己有序退出,由服务管理器拉起,不执行任何命令。 /// public sealed class ScheduleParams { [JsonPropertyName("restart_at")] public string? RestartAt { get; set; } } /// 宿主机更新器已经验证过的可用更新。 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; } } /// /// 宿主机更新器的状态(它写在 update-status/status.json,节点读出来随心跳上报): /// up_to_date、available、applying、failed、rolled_back、invalid(清单验签或校验没过)。 /// 没有安装更新器的节点没有这个字段。 /// public sealed class UpdateStatus { [JsonPropertyName("state")] public string State { get; set; } = ""; /// 正在运行的镜像 digest(更新器记录的;还没更新过时为空)。 [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; } } /// 节点的 fleet.heartbeat 附加字段:进程内的计数、拨号侧的线路健康和监听侧的会话快照,没有主机信息。 public sealed class NodeHeartbeatExtra { /// Node operating state; registration approval and transport presence are reported separately. [JsonPropertyName("runtime_status")] public NodeRuntimeStatus RuntimeStatus { get; set; } = NodeRuntimeStatus.Running; /// 拨号侧:到每个监听节点(每个 IP 族)的线路健康。 [JsonPropertyName("links")] public List? Links { get; set; } [JsonPropertyName("ipv6")] public Ipv6Diagnostic? Ipv6 { get; set; } /// 监听侧:当前连着的拨号方,每次心跳都是完整的快照,Backend 靠它纠正错过的 dialer_connected / dialer_disconnected 事件。 [JsonPropertyName("dialers")] public List 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; } /// 日志只报计数(不带文本):自上次心跳以来各级别的条数、最多几个出现最多的类型,以及最新一条的编号;文本由 Backend 用 moshiro.logs.fetch 按需拉取。 [JsonPropertyName("log_summary")] public LogSummaryDto? LogSummary { get; set; } /// 自上次心跳以来自适应探测频率的状态变化(节点上限 512 条,超出丢最旧的)。 [JsonPropertyName("rate_changes")] public List? RateChanges { get; set; } } /// 某一类日志(消息文本的稳定哈希)在一个级别上出现了多少次。 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; } } /// /// 自适应探测频率的一次状态变化(Quiet / Watch / Active / Dead):节点随心跳上报,Backend 存成事件(rate_change), /// 事后可以回看每条线路、每个时段的探测频率为什么升降(例如晚高峰)。线路任务带 listener(对端)和 ip_version,目标任务带 task。 /// public sealed class RateChange { [JsonPropertyName("at_ms")] public long AtMs { get; set; } /// 这条线路的监听方节点 id(线路任务)。 [JsonPropertyName("listener")] public string? Listener { get; set; } /// 拨号方节点 id:节点上报时不填,由 Backend 按上报者补上。 [JsonPropertyName("dialer")] public string? Dialer { get; set; } [JsonPropertyName("ip_version")] public string? IpVersion { get; set; } /// 单个 Agent 任务的任务 id。 [JsonPropertyName("task")] public string? Task { get; set; } [JsonPropertyName("kind")] public string Kind { get; set; } = ""; [JsonPropertyName("from")] public string From { get; set; } = ""; [JsonPropertyName("to")] public string To { get; set; } = ""; [JsonPropertyName("hz")] public double Hz { get; set; } [JsonPropertyName("reason")] public string Reason { get; set; } = ""; /// 变化时刻证明窗口内的丢包率(百分比)和发出、丢失的包数。 [JsonPropertyName("loss_pct")] public double LossPct { get; set; } [JsonPropertyName("sent")] public int Sent { get; set; } [JsonPropertyName("lost")] public int Lost { 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 Types { get; set; } = []; } /// Backend → 节点:拉取编号大于 after_id 的日志(最旧的在前),级别不低于 min_level。 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 Fields { get; set; } = []; } public sealed class LogsFetchResult { [JsonPropertyName("items")] public List Items { get; set; } = []; /// 节点上最新一条的编号:比 after_id 小说明节点重启过,编号从头开始。 [JsonPropertyName("last_id")] public long LastId { get; set; } } /// /// 拨号方到一个监听方(一个 IP 族)的线路健康。控制层(TCP+TLS 长连接,承载控制 RPC 和回传数据)与任务层(测量用的 TCP / QUIC) /// 各有一个状态机:控制层断开才算两个节点失联,任务层只影响测量。 /// 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(); /// 控制层在线且 QUIC 数据通道就绪:这条线路可以完整测量。 [JsonIgnore] public bool Ready => Control.State == ControlState.Up && Task.Quic.State == QuicState.Ready; } /// 某一层最近一次失败的原因:失败发生的阶段、分类和说明。 public sealed class LinkFault { [JsonPropertyName("phase")] public string Phase { get; set; } = ""; /// timeout、refused、reset、dns、tls、auth、closed、silent、other。 [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}"; } /// 控制层:到 Agent 的 TCP+TLS 长连接,包括认证、时钟同步,以及回传数据的发件箱。 public sealed class ControlHealth { [JsonPropertyName("state")] public ControlState State { get; set; } = ControlState.Dialing; /// 进入当前状态的时间(Unix 毫秒)。 [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("outbox_bytes")] public long OutboxBytes { get; set; } [JsonPropertyName("outbox_pending")] public int OutboxPending { get; set; } } /// 任务层:测量自己使用的链路。TCP 类积木自带套接字和重拨,QUIC 数据通道由拨号方单独维护。 public sealed class TaskHealth { /// 正在运行的 TCP 类线路积木数(tcp_ping、connect、hold、tcp_transfer)。 [JsonPropertyName("tcp_blocks")] public int TcpBlocks { get; set; } [JsonPropertyName("quic")] public QuicHealth Quic { get; set; } = new(); } /// QUIC 数据通道(quic_ping、quic_stream 使用)的状态、握手统计和传输层统计。 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; } /// QUIC packet-loss estimate from MsQuic's non-spurious loss counters; not Datagram application loss or retransmission. [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; } } /// 控制层状态:dialing(DNS、TCP、敲门、TLS)→ authenticating(身份签名)→ syncing(时钟)→ up;失败进入 backoff。 [JsonConverter(typeof(JsonStringEnumConverter))] public enum ControlState { [JsonStringEnumMemberName("dialing")] Dialing, [JsonStringEnumMemberName("authenticating")] Authenticating, [JsonStringEnumMemberName("syncing")] Syncing, [JsonStringEnumMemberName("up")] Up, [JsonStringEnumMemberName("backoff")] Backoff, } /// QUIC 数据通道状态:控制层不在线时是 idle;connecting 握手中;ready 可发送;retrying 断开后等待重拨。 [JsonConverter(typeof(JsonStringEnumConverter))] public enum QuicState { [JsonStringEnumMemberName("idle")] Idle, [JsonStringEnumMemberName("connecting")] Connecting, [JsonStringEnumMemberName("ready")] Ready, [JsonStringEnumMemberName("retrying")] Retrying, } /// 节点的 fleet.hello 附加字段:属主、系统与构建信息,以及(监听时)面向拨号方的接入信息。 public sealed class NodeHelloMeta { /// 这个节点的属主:安装时固定在节点本地的 passkey 凭证 id。面板据此知道它绑定在哪个账户的哪把 passkey 上。 [JsonPropertyName("owners")] public List? 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; } /// 监听接入信息;节点不监听时为 null,面板不会把它当作拨号目标。 [JsonPropertyName("listen")] public ListenInfo? Listen { get; set; } } /// 节点作为监听方时,面向拨号方的接入信息。 public sealed class ListenInfo { [JsonPropertyName("tcp_port")] public int TcpPort { get; set; } [JsonPropertyName("quic_ports")] public List QuicPorts { get; set; } = []; /// 本机是否启用了 IPv6 监听;面板只给启用了的节点安排 IPv6 会话。 [JsonPropertyName("ipv6_supported")] public bool Ipv6Supported { get; set; } /// 本机的公网地址(IPv4 公网地址、IPv6 全球单播地址):面板据此自动填 public_host 和 ipv6_host,管理员手填的优先。 [JsonPropertyName("addresses")] public List? Addresses { get; set; } } /// /// 主机资源占用的只读快照(Agent → Backend,每隔几秒一次)。只有汇总的数字:没有 CPU 型号、主机名、网卡名,也没有任何 IP 地址。 /// 读不到的项是 null。 /// 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; } /// 所有物理网卡的累计收发字节(自开机以来)。 [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; } /// 已统计接口的内核报告链路速率之和(Mbit/s);驱动不支持或无法读取时为空,不代表互联网带宽。 [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; } } /// 监听方眼中的一个已连接拨号方。 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; } /// 拨号方的远端地址(经过 NAT 之后的公网 IP:端口)。 [JsonPropertyName("remote")] public string? Remote { get; set; } [JsonPropertyName("since_utc_ms")] public long SinceUtcMs { get; set; } } /// 监听方下发给拨号方的一条线路任务(期望状态类型 line_task_set 里的一项)。 public sealed class LineTask { [JsonPropertyName("id")] public TaskId Id { get; set; } [JsonPropertyName("kind")] public string Kind { get; set; } = ""; [JsonPropertyName("params")] public Dictionary Params { get; set; } = []; /// 数据面用它区分任务;Agent 在本进程生命周期内分配。 [JsonPropertyName("stream_id")] public uint StreamId { get; set; } [JsonPropertyName("port")] public int Port { get; set; } } /// 这条线路的期望状态(监听方 → 拨号方):拨号方按它调和自己一侧的角色。 public sealed class LineTaskSet { public const string StateType = "line_task_set"; [JsonPropertyName("version")] public long Version { get; set; } [JsonPropertyName("tasks")] public List Tasks { get; set; } = []; /// 监听方的数据面端口(拨号方把包发到这里)。 [JsonPropertyName("quic_ports")] public List QuicPorts { get; set; } = []; [JsonPropertyName("tcp_ports")] public List TcpPorts { get; set; } = []; } /// moshiro.clock:拨号方用它估计与监听方单调时钟的偏移(对称往返假设)。 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; } } /// 按需测试的一个阶段(监听方 → 拨号方):监听方编排时间,拨号方准备好自己一侧的角色之后应答。 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 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; } } /// 阶段结束(监听方 → 拨号方):拨号方收尾并把自己一侧的列块交给监听方。sent_count 是监听方一侧发出的包数(单向数据流用)。 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; } } /// 拨号方收尾之后回复:它交给监听方的列块个数(监听方据此等待全部列块到齐再进入下一阶段)。 public sealed class TestPhaseEndResult { [JsonPropertyName("fragments")] public int Fragments { get; set; } } public sealed class TestAbortParams { [JsonPropertyName("test_id")] public TestId TestId { get; set; } } /// 拨号方一侧的列块放在 RawChunk 头部 params 里的保留键:本块覆盖的序号范围 [from, to)(单向数据流由监听方一侧按它取行)。 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(RateChange))] [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;