package model // AgentNode binds a lightweight agent (token-registered, no web panel) to a // 3x-ui node row and carries the per-agent egress configuration. // // Agents are the only writers of their own LastSeen; the panel owns everything // else. Authentication uses only the SHA-256 hex of the token; since the // next-core batch (item 19) the raw token is additionally kept recoverably // in TokenPlain — an operator-accepted tradeoff so the node edit page can // re-display the full install command. type AgentNode struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` TokenHash string `json:"-" gorm:"size:64;uniqueIndex"` TokenPrefix string `json:"tokenPrefix" gorm:"size:16;index"` // TokenPlain is a recoverable copy of the raw token: stored at // issuance, and backfilled from the first authenticated agent request // for rows created before this field existed (item 19). Existing // values are never overwritten by requests; only issuance (create / // panel-side re-issue) writes a fresh one. Never serialized. TokenPlain string `json:"-" gorm:"type:text"` Name string `json:"name"` // NodeID is the 3x-ui node whose inbounds this agent serves. Nil means the // token was issued but not yet bound to a node. NodeID *int `json:"nodeId" gorm:"index"` // Outbounds is the full xray "outbounds" JSON array pushed to this agent, // replacing the template defaults. Empty means "use template defaults". Outbounds string `json:"outbounds" gorm:"type:text"` // RoutingRules is the full xray "routing.rules" JSON array pushed to this // agent, replacing the template defaults. Empty means "use template defaults". RoutingRules string `json:"routingRules" gorm:"type:text"` // SingboxOutbounds / SingboxRoutingRules are the sing-box conversions // of the two override columns above, produced by the B1 one-shot // migration (ITEM6 §5.6). Entries that cannot be mapped stay only in // the legacy columns and are listed in the migration report. The // legacy columns stay the read source until B2. SingboxOutbounds string `json:"-" gorm:"column:singbox_outbounds;type:text"` SingboxRoutingRules string `json:"-" gorm:"column:singbox_routing_rules;type:text"` LastSeen int64 `json:"lastSeen"` CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"` UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // Heartbeat telemetry (P13): the agent piggybacks host/runtime facts on // its 60s heartbeat and the panel overlays them onto the node list row. // All fields are best-effort: an agent that never reported telemetry has // TelemetryAt == 0 and the UI keeps showing "-" for its row. AgentVersion string `json:"agentVersion"` XrayVersion string `json:"xrayVersion"` // SingboxVersion is the agent's sing-box core version (V2.0.1 ②): // reported on the same telemetry since ITEM6 B3, persisted only // from V2.0.1 on — the node list shows it in place of the xray // version for agents that report one. SingboxVersion string `json:"singboxVersion"` UptimeSecs uint64 `json:"uptimeSecs"` LatencyMs int `json:"latencyMs"` CpuPct float64 `json:"cpuPct"` MemPct float64 `json:"memPct"` IPv4 string `json:"ipv4"` IPv6 string `json:"ipv6"` // TelemetryAt is the unix-milli time of the last heartbeat that carried // a telemetry object. TelemetryAt int64 `json:"telemetryAt"` // Probe snapshot (item 7, B1): the monitor-probe-style host Facts and // latest Metrics the agent piggybacks on register + heartbeat. Only the // latest values live here; history lands in B2's probe tables. Agents // that never reported a probe block keep ProbeAt == 0 and zero values. ProbeHostname string `json:"probeHostname"` ProbeOS string `json:"probeOs"` ProbeKernel string `json:"probeKernel"` ProbeArch string `json:"probeArch"` ProbeVirt string `json:"probeVirt"` ProbeCPUName string `json:"probeCpuName"` ProbeCPUCores uint32 `json:"probeCpuCores"` ProbeBootID string `json:"probeBootId"` ProbeIface string `json:"probeIface"` ProbeLoad1 float32 `json:"probeLoad1"` ProbeLoad5 float32 `json:"probeLoad5"` ProbeLoad15 float32 `json:"probeLoad15"` ProbeMemTotal uint64 `json:"probeMemTotal"` ProbeMemUsed uint64 `json:"probeMemUsed"` ProbeSwapTotal uint64 `json:"probeSwapTotal"` ProbeSwapUsed uint64 `json:"probeSwapUsed"` ProbeDiskTotal uint64 `json:"probeDiskTotal"` ProbeDiskUsed uint64 `json:"probeDiskUsed"` ProbeNetRxTot uint64 `json:"probeNetRxTotal"` ProbeNetTxTot uint64 `json:"probeNetTxTotal"` ProbeNetRx uint64 `json:"probeNetRx"` ProbeNetTx uint64 `json:"probeNetTx"` ProbeTCP uint32 `json:"probeTcp"` ProbeUDP uint32 `json:"probeUdp"` ProbeProcs uint32 `json:"probeProcs"` ProbeCPU float32 `json:"probeCpu"` // ProbeAt is the unix-milli time of the last heartbeat that carried a // probe metrics block. ProbeAt int64 `json:"probeAt"` // Seen source addresses (public-address resolution): the source IP of // the agent's own authenticated requests, as observed by the master — // the CF-Connecting-IP header when the agent comes through Cloudflare, // else the direct peer address. An agent behind 1:1 NAT reports only // its private interface address in telemetry (its public address is // not assigned to any local interface, so enumeration can never see // it); the observed source address is the master's view of the // agent's public side. Writes are change-only (see // AgentController.captureSeenIP); SeenIPAt is the unix-milli time of // the last change, not of the last request. SeenIPv4 string `json:"seenIpv4"` SeenIPv6 string `json:"seenIpv6"` SeenIPAt int64 `json:"seenIpAt"` // ProbeOnly marks the node as probe-only (V2.1.0): the master serves // no core config for it and the agent runs no core at all — only the // probe channels (heartbeat, fast report, latency tasks) stay live. // Set from the panel's node form; the agent can also preset it in // its local agent.json, either source turns the mode on. ProbeOnly bool `json:"probeOnly"` // One-click update (item 20): the master records an update intent here; // the agent picks it up from its config poll, downloads + verifies + // swaps its binary, restarts itself and reports the outcome back on // the heartbeat. UpdateRequestedAt == 0 means no intent is pending. UpdateRequestedAt int64 `json:"updateRequestedAt"` UpdateTarget string `json:"updateTarget"` // A-form display target, e.g. "A1.0.1" UpdateTag string `json:"updateTag"` // release tag to download, e.g. "V1.0.1" // UpdateState: "" | pending | downloading | swapped | failed | // current | success (success = new A version observed by the master). UpdateState string `json:"updateState"` UpdateError string `json:"updateError"` UpdateStateAt int64 `json:"updateStateAt"` }