Compare commits
2 Commits
1.0.2
...
1.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
| a7e51c5bea | |||
| 07adc3ac5c |
@@ -157,49 +157,90 @@ func (b *Builder) Build() models.TopologyGraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略3b: 通过MAC前缀匹配(改进:排除歧义前缀)
|
// 策略3b: MAC前缀匹配(使用12字符前缀 - 更精确)
|
||||||
// 当精确MAC匹配失败时,尝试通过MAC前缀匹配
|
// 同品牌设备虽然4字符前缀相同,但12字符前缀通常不同
|
||||||
// 但如果同一前缀匹配到多台设备,则跳过(避免错误连接)
|
|
||||||
if targetIP == "" && neighbor.RemoteMAC != "" {
|
if targetIP == "" && neighbor.RemoteMAC != "" {
|
||||||
neighborMACPrefix := getMACPrefix(neighbor.RemoteMAC)
|
// 使用12字符前缀(例如:642f-c7e0-03)
|
||||||
fmt.Printf(" Trying MAC prefix match: %s (prefix: %s)\n", neighbor.RemoteMAC, neighborMACPrefix)
|
macPrefix := ""
|
||||||
|
if len(neighbor.RemoteMAC) >= 12 {
|
||||||
// 先统计有多少台设备匹配此MAC前缀
|
macPrefix = neighbor.RemoteMAC[:12]
|
||||||
type prefixMatch struct {
|
} else {
|
||||||
ip string
|
macPrefix = neighbor.RemoteMAC
|
||||||
matchingMACs int
|
|
||||||
}
|
}
|
||||||
var matches []prefixMatch
|
|
||||||
|
fmt.Printf(" Trying MAC prefix match (12 chars): %s (prefix: %s)\n", neighbor.RemoteMAC, macPrefix)
|
||||||
|
|
||||||
|
var matchedDevices []string
|
||||||
|
|
||||||
for _, d := range b.devices {
|
for _, d := range b.devices {
|
||||||
if d.IP == device.IP {
|
if d.IP == device.IP {
|
||||||
continue // 跳过自己
|
continue // 跳过自己
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查设备d的MAC地址是否有匹配的前缀
|
||||||
matchingMACs := 0
|
matchingMACs := 0
|
||||||
for _, mac := range d.MACAddresses {
|
for _, mac := range d.MACAddresses {
|
||||||
if getMACPrefix(mac) == neighborMACPrefix {
|
if len(mac) >= 12 && mac[:12] == macPrefix {
|
||||||
matchingMACs++
|
matchingMACs++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if matchingMACs >= 3 {
|
if matchingMACs >= 3 {
|
||||||
matches = append(matches, prefixMatch{ip: d.IP, matchingMACs: matchingMACs})
|
matchedDevices = append(matchedDevices, d.IP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只在唯一匹配时使用前缀匹配
|
// 只有唯一匹配时才建立连接
|
||||||
if len(matches) == 1 && getSubnet(matches[0].ip) == getSubnet(device.IP) {
|
if len(matchedDevices) == 1 {
|
||||||
targetIP = matches[0].ip
|
targetIP = matchedDevices[0]
|
||||||
matchMethod = fmt.Sprintf("MAC-prefix(%s)", neighborMACPrefix)
|
matchMethod = fmt.Sprintf("MAC-prefix-12(%s)", macPrefix)
|
||||||
fmt.Printf(" ✓ Matched by MAC prefix: %s (device has %d MACs with prefix %s, same subnet) -> %s\n",
|
fmt.Printf(" ✓ Matched by MAC prefix-12 (unique): %s -> %s\n", neighbor.RemoteMAC, targetIP)
|
||||||
neighbor.RemoteMAC, matches[0].matchingMACs, neighborMACPrefix, targetIP)
|
} else if len(matchedDevices) > 1 {
|
||||||
} else if len(matches) > 1 {
|
fmt.Printf(" ✗ Ambiguous MAC prefix-12 match: %s matched %v\n", macPrefix, matchedDevices)
|
||||||
fmt.Printf(" ✗ Skipping MAC prefix match: %d devices share prefix %s (ambiguous)\n", len(matches), neighborMACPrefix)
|
} else {
|
||||||
|
fmt.Printf(" ✗ No device matches MAC prefix-12 %s\n", macPrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略3c: LLDP接口匹配(改进 - 单向但要求唯一性)
|
||||||
|
// 如果设备A的LLDP信息显示它连接到接口Y
|
||||||
|
// 而设备B有接口Y,则A连接到B
|
||||||
|
// 但如果有多个设备都有接口Y,则不匹配(避免歧义)
|
||||||
|
if targetIP == "" && neighbor.RemoteMAC != "" && neighbor.RemoteInterface != "" {
|
||||||
|
fmt.Printf(" Trying LLDP interface match: looking for device with interface %s\n", neighbor.RemoteInterface)
|
||||||
|
|
||||||
|
var matchedDevices []string
|
||||||
|
|
||||||
|
for _, d := range b.devices {
|
||||||
|
if d.IP == device.IP {
|
||||||
|
continue // 跳过自己
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查设备d是否有这个接口
|
||||||
|
for _, iface := range d.Interfaces {
|
||||||
|
if iface.Name == neighbor.RemoteInterface {
|
||||||
|
matchedDevices = append(matchedDevices, d.IP)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有唯一匹配时才建立连接
|
||||||
|
if len(matchedDevices) == 1 {
|
||||||
|
targetIP = matchedDevices[0]
|
||||||
|
matchMethod = fmt.Sprintf("LLDP-interface(%s->%s)", neighbor.LocalInterface, neighbor.RemoteInterface)
|
||||||
|
fmt.Printf(" ✓ Matched by LLDP interface (unique): %s %s -> %s %s\n",
|
||||||
|
device.IP, neighbor.LocalInterface, targetIP, neighbor.RemoteInterface)
|
||||||
|
} else if len(matchedDevices) > 1 {
|
||||||
|
fmt.Printf(" ✗ Ambiguous: multiple devices have interface %s: %v\n",
|
||||||
|
neighbor.RemoteInterface, matchedDevices)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✗ No device found with interface %s\n", neighbor.RemoteInterface)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略4: 通过本地接口IP网段匹配(新增)
|
// 策略4: 通过本地接口IP网段匹配(新增)
|
||||||
|
// 注意:此策略容易产生误匹配,仅在必要时使用
|
||||||
if targetIP == "" {
|
if targetIP == "" {
|
||||||
// 查找本地接口的IP地址
|
// 查找本地接口的IP地址
|
||||||
localInterfaceIP := ""
|
localInterfaceIP := ""
|
||||||
@@ -211,7 +252,7 @@ func (b *Builder) Build() models.TopologyGraph {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if localInterfaceIP != "" {
|
if localInterfaceIP != "" {
|
||||||
fmt.Printf(" Trying subnet match: local interface %s has IP %s\n",
|
fmt.Printf(" ⚠ Trying subnet match (risky): local interface %s has IP %s\n",
|
||||||
neighbor.LocalInterface, localInterfaceIP)
|
neighbor.LocalInterface, localInterfaceIP)
|
||||||
|
|
||||||
// 计算本地接口的网段
|
// 计算本地接口的网段
|
||||||
@@ -227,7 +268,7 @@ func (b *Builder) Build() models.TopologyGraph {
|
|||||||
if getSubnet(d.IP) == localSubnet {
|
if getSubnet(d.IP) == localSubnet {
|
||||||
targetIP = d.IP
|
targetIP = d.IP
|
||||||
matchMethod = "subnet(management IP)"
|
matchMethod = "subnet(management IP)"
|
||||||
fmt.Printf(" ✓ Matched by subnet: %s in %s\n", d.IP, localSubnet)
|
fmt.Printf(" ⚠ Matched by subnet (risky): %s in %s\n", d.IP, localSubnet)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +277,7 @@ func (b *Builder) Build() models.TopologyGraph {
|
|||||||
if iface.IP != "" && getSubnet(iface.IP) == localSubnet {
|
if iface.IP != "" && getSubnet(iface.IP) == localSubnet {
|
||||||
targetIP = d.IP
|
targetIP = d.IP
|
||||||
matchMethod = fmt.Sprintf("subnet(interface %s)", iface.Name)
|
matchMethod = fmt.Sprintf("subnet(interface %s)", iface.Name)
|
||||||
fmt.Printf(" ✓ Matched by subnet: %s (%s) in %s\n",
|
fmt.Printf(" ⚠ Matched by subnet (risky): %s (%s) in %s\n",
|
||||||
d.IP, iface.Name, localSubnet)
|
d.IP, iface.Name, localSubnet)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -246,27 +287,9 @@ func (b *Builder) Build() models.TopologyGraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 策略4b: 本地接口没有IP,尝试使用设备管理IP进行子网匹配(新增)
|
// 策略4b: 本地接口没有IP,尝试使用设备管理IP进行子网匹配(高风险策略 - 已禁用)
|
||||||
// 注意:只有当该网段只有2台设备时才使用此策略(点对点连接)
|
// 此策略容易产生错误连接,暂时禁用
|
||||||
fmt.Printf(" Trying subnet match with management IP: %s\n", device.IP)
|
fmt.Printf(" ✗ Skipping subnet match with management IP (disabled - too risky)\n")
|
||||||
localSubnet := getSubnet(device.IP)
|
|
||||||
|
|
||||||
// 统计在该网段的设备数量
|
|
||||||
var devicesInSubnet []string
|
|
||||||
for _, d := range b.devices {
|
|
||||||
if d.IP != device.IP && getSubnet(d.IP) == localSubnet {
|
|
||||||
devicesInSubnet = append(devicesInSubnet, d.IP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有当网段中恰好有1台其他设备时才匹配(点对点)
|
|
||||||
if len(devicesInSubnet) == 1 {
|
|
||||||
targetIP = devicesInSubnet[0]
|
|
||||||
matchMethod = "subnet(management IP both sides)"
|
|
||||||
fmt.Printf(" ✓ Matched by management subnet: %s in %s (only device in subnet)\n", targetIP, localSubnet)
|
|
||||||
} else if len(devicesInSubnet) > 1 {
|
|
||||||
fmt.Printf(" ✗ Skipping subnet match: %d devices in subnet %s (ambiguous)\n", len(devicesInSubnet)+1, localSubnet)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ header h1 {
|
|||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 600px;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -190,6 +192,7 @@ header h1 {
|
|||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cy {
|
#cy {
|
||||||
@@ -200,17 +203,47 @@ header h1 {
|
|||||||
|
|
||||||
.detail-panel {
|
.detail-panel {
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 1000px;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
|
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
|
||||||
display: none;
|
display: none;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-panel.active {
|
.detail-panel.active {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 拖拽手柄样式 */
|
||||||
|
.resizer {
|
||||||
|
width: 5px;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: #e0e0e0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer:hover,
|
||||||
|
.resizer.resizing {
|
||||||
|
background: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 30px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-panel h3 {
|
.detail-panel h3 {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
|
|||||||
+7
-1
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar" id="sidebar">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>扫描配置</h3>
|
<h3>扫描配置</h3>
|
||||||
<form id="scan-form">
|
<form id="scan-form">
|
||||||
@@ -69,11 +69,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- 左侧拖拽手柄 -->
|
||||||
|
<div class="resizer" id="resizer-left"></div>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<div id="cy"></div>
|
<div id="cy"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- 右侧拖拽手柄 -->
|
||||||
|
<div class="resizer" id="resizer-right"></div>
|
||||||
|
|
||||||
<!-- 详情面板 -->
|
<!-- 详情面板 -->
|
||||||
<aside class="detail-panel" id="detail-panel">
|
<aside class="detail-panel" id="detail-panel">
|
||||||
<h3>设备详情</h3>
|
<h3>设备详情</h3>
|
||||||
|
|||||||
@@ -908,3 +908,81 @@ async function saveEditDevice(event) {
|
|||||||
alert('修改失败: ' + error.message);
|
alert('修改失败: ' + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 面板拖拽调整大小功能
|
||||||
|
(function() {
|
||||||
|
// 左侧面板拖拽
|
||||||
|
var resizerLeft = document.getElementById('resizer-left');
|
||||||
|
var sidebar = document.getElementById('sidebar');
|
||||||
|
|
||||||
|
if (resizerLeft && sidebar) {
|
||||||
|
var isResizingLeft = false;
|
||||||
|
|
||||||
|
resizerLeft.addEventListener('mousedown', function(e) {
|
||||||
|
isResizingLeft = true;
|
||||||
|
resizerLeft.classList.add('resizing');
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', function(e) {
|
||||||
|
if (!isResizingLeft) return;
|
||||||
|
|
||||||
|
var newWidth = e.clientX;
|
||||||
|
var minWidth = 200;
|
||||||
|
var maxWidth = 600;
|
||||||
|
|
||||||
|
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||||
|
sidebar.style.width = newWidth + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', function() {
|
||||||
|
if (isResizingLeft) {
|
||||||
|
isResizingLeft = false;
|
||||||
|
resizerLeft.classList.remove('resizing');
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧面板拖拽
|
||||||
|
var resizerRight = document.getElementById('resizer-right');
|
||||||
|
var detailPanel = document.getElementById('detail-panel');
|
||||||
|
|
||||||
|
if (resizerRight && detailPanel) {
|
||||||
|
var isResizingRight = false;
|
||||||
|
|
||||||
|
resizerRight.addEventListener('mousedown', function(e) {
|
||||||
|
isResizingRight = true;
|
||||||
|
resizerRight.classList.add('resizing');
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', function(e) {
|
||||||
|
if (!isResizingRight) return;
|
||||||
|
|
||||||
|
var containerWidth = document.querySelector('.main-content').offsetWidth;
|
||||||
|
var newWidth = containerWidth - e.clientX;
|
||||||
|
var minWidth = 300;
|
||||||
|
var maxWidth = 1000;
|
||||||
|
|
||||||
|
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||||
|
detailPanel.style.width = newWidth + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', function() {
|
||||||
|
if (isResizingRight) {
|
||||||
|
isResizingRight = false;
|
||||||
|
resizerRight.classList.remove('resizing');
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user