fix(onlines): agent 在线名单按节点归因键存储(第12条修复A)
reportTraffic 旧实现把所有 agent 的在线名单写死存入 "agent" 键,
而入站页按 originNodeGuid / node:{id} 归因键查找,永远查不到:
agent 入站行永不亮在线,且所有 agent 节点被并进同一键。
改为 InboundService.SetAgentOnlineClients:按节点的 effectiveNodeKey
(有唯一 GUID 用 GUID、否则 node:{id})存入;空名单仍走
ClearNodeOnlineClients 清整棵子树。同步修正 online_store_test 中
断言旧键的用例注释与键名;新增两例服务层测试(双 agent 节点键互不
合并、空上报清空)。
This commit is contained in:
@@ -217,13 +217,7 @@ func (a *AgentController) reportTraffic(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if agent.NodeID != nil {
|
if agent.NodeID != nil {
|
||||||
if len(req.Onlines) > 0 {
|
a.inboundService.SetAgentOnlineClients(*agent.NodeID, req.Onlines)
|
||||||
a.inboundService.SetNodeOnlineTree(*agent.NodeID, map[string][]string{
|
|
||||||
"agent": req.Onlines,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
a.inboundService.ClearNodeOnlineClients(*agent.NodeID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
a.touchAgent(uint(agent.Id), "")
|
a.touchAgent(uint(agent.Id), "")
|
||||||
jsonObj(c, gin.H{"message": "ok"}, nil)
|
jsonObj(c, gin.H{"message": "ok"}, nil)
|
||||||
|
|||||||
@@ -1366,6 +1366,31 @@ func (s *InboundService) SetNodeOnlineTree(nodeID int, tree map[string][]string)
|
|||||||
sharedOnlineStore.SetNodeOnlineTree(nodeID, tree)
|
sharedOnlineStore.SetNodeOnlineTree(nodeID, tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAgentOnlineClients records the online clients an agent node reports in
|
||||||
|
// its traffic payload. An agent has no GUID tree of its own, so its clients
|
||||||
|
// are keyed under the node's effective attribution key (effectiveNodeKey —
|
||||||
|
// the same key setRemoteTrafficLocked computes for panel nodes), which is
|
||||||
|
// what the inbounds page looks up via the inbound's originNodeGuid /
|
||||||
|
// node:{id} fallback. Storing them under a literal "agent" key (the old
|
||||||
|
// controller behaviour) left agent inbounds permanently dark on that page.
|
||||||
|
// An empty list is the agent's disconnect signal and clears the node's
|
||||||
|
// whole online contribution.
|
||||||
|
func (s *InboundService) SetAgentOnlineClients(nodeID int, onlines []string) {
|
||||||
|
if len(onlines) == 0 {
|
||||||
|
s.ClearNodeOnlineClients(nodeID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := synthNodeGuid(nodeID)
|
||||||
|
var nodeRow model.Node
|
||||||
|
if err := database.GetDB().Select("guid").Where("id = ?", nodeID).First(&nodeRow).Error; err == nil {
|
||||||
|
nodeRow.Id = nodeID
|
||||||
|
if k := effectiveNodeKey(&nodeRow); k != "" {
|
||||||
|
key = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.SetNodeOnlineTree(nodeID, map[string][]string{key: onlines})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) SetNodeActiveInboundTree(nodeID int, tree map[string][]string) {
|
func (s *InboundService) SetNodeActiveInboundTree(nodeID int, tree map[string][]string) {
|
||||||
sharedOnlineStore.SetNodeActiveInboundTree(nodeID, tree)
|
sharedOnlineStore.SetNodeActiveInboundTree(nodeID, tree)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Agent nodes report their online clients inside the traffic payload; the
|
||||||
|
// master files them via InboundService.SetAgentOnlineClients. They must be
|
||||||
|
// keyed under the node's effective attribution key — pre-fix the controller
|
||||||
|
// stored every agent's list under one literal "agent" key, which the
|
||||||
|
// inbounds page (lookup by originNodeGuid / node:{id}) never matches, so
|
||||||
|
// agent inbounds never lit anyone online and all agents merged into one
|
||||||
|
// bucket. These tests pin the per-node keying.
|
||||||
|
func TestSetAgentOnlineClients_AttributionKeysPerNode(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
db := database.GetDB()
|
||||||
|
svc := InboundService{}
|
||||||
|
|
||||||
|
const nodeA = 7301
|
||||||
|
const nodeB = 7302
|
||||||
|
for _, id := range []int{nodeA, nodeB} {
|
||||||
|
svc.ClearNodeOnlineClients(id)
|
||||||
|
if err := db.Create(&model.Node{Id: id, Name: fmt.Sprintf("agent-node-%d", id), Address: "10.0.7.3"}).Error; err != nil {
|
||||||
|
t.Fatalf("create node %d: %v", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
svc.ClearNodeOnlineClients(nodeA)
|
||||||
|
svc.ClearNodeOnlineClients(nodeB)
|
||||||
|
})
|
||||||
|
|
||||||
|
svc.SetAgentOnlineClients(nodeA, []string{"agent-a@example.com"})
|
||||||
|
svc.SetAgentOnlineClients(nodeB, []string{"agent-b@example.com"})
|
||||||
|
|
||||||
|
keyA := effectiveNodeKey(&model.Node{Id: nodeA})
|
||||||
|
keyB := effectiveNodeKey(&model.Node{Id: nodeB})
|
||||||
|
if keyA == keyB {
|
||||||
|
t.Fatalf("effective keys collide: %q", keyA)
|
||||||
|
}
|
||||||
|
|
||||||
|
byGuid := svc.GetOnlineClientsByGuid()
|
||||||
|
assertStringSet(t, "byGuid node A", byGuid[keyA], []string{"agent-a@example.com"})
|
||||||
|
assertStringSet(t, "byGuid node B", byGuid[keyB], []string{"agent-b@example.com"})
|
||||||
|
if _, ok := byGuid["agent"]; ok {
|
||||||
|
t.Fatalf("byGuid still carries the legacy literal \"agent\" key: %v", byGuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
union := svc.GetOnlineClients()
|
||||||
|
for _, want := range []string{"agent-a@example.com", "agent-b@example.com"} {
|
||||||
|
found := false
|
||||||
|
for _, got := range union {
|
||||||
|
if got == want {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("online union %v missing %q", union, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty onlines list is the agent's disconnect signal: the node's
|
||||||
|
// clients must stop showing as online immediately.
|
||||||
|
func TestSetAgentOnlineClients_EmptyReportClears(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
db := database.GetDB()
|
||||||
|
svc := InboundService{}
|
||||||
|
|
||||||
|
const nodeID = 7303
|
||||||
|
svc.ClearNodeOnlineClients(nodeID)
|
||||||
|
t.Cleanup(func() { svc.ClearNodeOnlineClients(nodeID) })
|
||||||
|
if err := db.Create(&model.Node{Id: nodeID, Name: "agent-node-7303", Address: "10.0.7.4"}).Error; err != nil {
|
||||||
|
t.Fatalf("create node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.SetAgentOnlineClients(nodeID, []string{"gone@example.com"})
|
||||||
|
key := effectiveNodeKey(&model.Node{Id: nodeID})
|
||||||
|
assertStringSet(t, "byGuid before clear", svc.GetOnlineClientsByGuid()[key], []string{"gone@example.com"})
|
||||||
|
|
||||||
|
svc.SetAgentOnlineClients(nodeID, nil)
|
||||||
|
if got := svc.GetOnlineClientsByGuid()[key]; len(got) != 0 {
|
||||||
|
t.Fatalf("byGuid[%q] after empty report = %v, want empty", key, got)
|
||||||
|
}
|
||||||
|
for _, got := range svc.GetOnlineClients() {
|
||||||
|
if got == "gone@example.com" {
|
||||||
|
t.Fatalf("online union still contains cleared client: %v", svc.GetOnlineClients())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,13 +55,15 @@ func TestOnlineStoreSetTreeUnionAndByGuid(t *testing.T) {
|
|||||||
requireSet(t, "local", st.GetLocalOnlineClients(), "alice@example.com", "local@example.com")
|
requireSet(t, "local", st.GetLocalOnlineClients(), "alice@example.com", "local@example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The agent controller reports onlines under the literal "agent" GUID key;
|
// Agent-node onlines arrive keyed by the node's attribution key (the
|
||||||
|
// synthNodeGuid form below for a GUID-less agent node — the controller used
|
||||||
|
// to file them under a literal "agent" key no inbound lookup ever matched);
|
||||||
// they must reach the union untouched.
|
// they must reach the union untouched.
|
||||||
func TestOnlineStoreAgentKeyTree(t *testing.T) {
|
func TestOnlineStoreAgentKeyTree(t *testing.T) {
|
||||||
st := &onlineStore{}
|
st := &onlineStore{}
|
||||||
st.SetNodeOnlineTree(7, map[string][]string{"agent": {"agentuser@example.com"}})
|
st.SetNodeOnlineTree(7, map[string][]string{"node:7": {"agentuser@example.com"}})
|
||||||
requireSet(t, "union", st.GetOnlineClients(), "agentuser@example.com")
|
requireSet(t, "union", st.GetOnlineClients(), "agentuser@example.com")
|
||||||
requireSet(t, "byGuid agent", st.GetMergedNodeTrees()["agent"], "agentuser@example.com")
|
requireSet(t, "byGuid node:7", st.GetMergedNodeTrees()["node:7"], "agentuser@example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setting a tree replaces the node's previous contribution; an empty tree
|
// Setting a tree replaces the node's previous contribution; an empty tree
|
||||||
|
|||||||
Reference in New Issue
Block a user