Files
3x-ui-agent/internal/database/model/agent_node.go
T
Muse 39ad8b8a2e ITEM6 B1: sing-box data model + one-shot migration
New stored model (legacy columns untouched, read paths unchanged
until B2):
- inbounds.singbox_config (converted sing-box inbound/endpoint
  fragment JSON; empty = blocked or external-runtime) and
  inbounds.singbox_preserved (unmapped items verbatim + panel-side
  material + notes)
- agent_nodes.singbox_outbounds / singbox_routing_rules

Converter (internal/singbox, pure functions, ITEM6-CONVERSION-SPEC):
all panel protocols mapped; unmapped constructs are preserved and
registered, never dropped. B0 golden fixtures: 26/26 semantic match
(fragment + unmapped concept/pending sets + disposition).

One-shot migrator (internal/database, InitDB hook after initModels):
marker-gated and idempotent; SQLite file backup before any write
(failure refuses migration/startup); stats switches migrate out of
the template BEFORE the xrayTemplateConfig row is discarded
(SCOPE 4.3.1 order); identity layer is the only client truth with
spec 7-1 mirror reconciliation (adopt/fill/conflict all reported);
fallbacks merged from table + embedded settings, deduped; agent
overrides converted with per-entry issues; everything in one
transaction, marker written last; JSON report written next to the
DB. Malformed rows/templates are hard failures (rollback, old DB
untouched).

settings.trafficStatsSwitches added with factory defaults
(inbound on, outbound off) in defaultValueMap.

Validation: synthetic agent-era DB (mirror drift, fallbacks, xhttp,
agent geo rule, non-default stats) converts as specified; old
production backup (25 inbounds) migrates with credentials, Reality
pbk derivation, traffic counters and reset cycles byte-identical;
idempotent re-run is a no-op; touched packages show zero new test
failures vs the 9-red baseline.
2026-10-22 19:32:15 +00:00

85 lines
4.6 KiB
Go

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"`
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"`
// 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"`
// 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"`
}