325 lines
13 KiB
TypeScript
325 lines
13 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import { initializeNetwork, clearNetworkCache } from '@/lib/network'
|
||
|
||
interface NetworkDebugInfo {
|
||
success: boolean
|
||
isIntranet: boolean
|
||
isPublicDomain: boolean
|
||
clientIp: string
|
||
clientIpSource: string
|
||
hostname: string
|
||
debug: {
|
||
forwardedIpList: string[]
|
||
hasInternalIpInForwarded: boolean
|
||
realIp: string | null
|
||
cfConnectingIp: string | null
|
||
xClientIp: string | null
|
||
clientIpIsInternal: boolean | null
|
||
}
|
||
timestamp: string
|
||
}
|
||
|
||
export default function NetworkDiagnosePage() {
|
||
const [loading, setLoading] = useState(true)
|
||
const [networkInfo, setNetworkInfo] = useState<NetworkDebugInfo | null>(null)
|
||
const [frontendStatus, setFrontendStatus] = useState<{ isInternal: boolean; host: string } | null>(null)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [testResults, setTestResults] = useState<{
|
||
internalMinIO: { success: boolean; time: number; error?: string } | null
|
||
publicS3: { success: boolean; time: number; error?: string } | null
|
||
}>({ internalMinIO: null, publicS3: null })
|
||
|
||
const fetchNetworkStatus = async () => {
|
||
setLoading(true)
|
||
setError(null)
|
||
|
||
try {
|
||
// 清除前端缓存,强制重新检测
|
||
clearNetworkCache()
|
||
|
||
// 获取前端网络状态
|
||
const frontendResult = await initializeNetwork(true)
|
||
setFrontendStatus(frontendResult)
|
||
|
||
// 获取后端网络状态(带诊断信息)
|
||
const response = await fetch('/api/network-status', { cache: 'no-store' })
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setNetworkInfo(data)
|
||
} else {
|
||
setError('获取网络状态失败')
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '未知错误')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 测试内网MinIO连接
|
||
const testInternalMinIO = async () => {
|
||
setTestResults(prev => ({ ...prev, internalMinIO: null }))
|
||
const startTime = Date.now()
|
||
|
||
try {
|
||
// 尝试访问内网MinIO的一个测试图片
|
||
const testUrl = 'http://192.168.58.20:9000/gongxiangpan/由壹点AI/test-connection.txt'
|
||
const response = await fetch(testUrl, {
|
||
method: 'HEAD',
|
||
mode: 'no-cors', // 跨域请求
|
||
cache: 'no-store'
|
||
})
|
||
|
||
const time = Date.now() - startTime
|
||
// no-cors 模式下无法读取响应状态,但如果没有抛出错误,说明网络可达
|
||
setTestResults(prev => ({
|
||
...prev,
|
||
internalMinIO: { success: true, time, error: '网络可达(无法确认文件是否存在)' }
|
||
}))
|
||
} catch (err) {
|
||
const time = Date.now() - startTime
|
||
setTestResults(prev => ({
|
||
...prev,
|
||
internalMinIO: {
|
||
success: false,
|
||
time,
|
||
error: err instanceof Error ? err.message : '连接失败'
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
|
||
// 测试公网S3连接
|
||
const testPublicS3 = async () => {
|
||
setTestResults(prev => ({ ...prev, publicS3: null }))
|
||
const startTime = Date.now()
|
||
|
||
try {
|
||
// 尝试访问公网S3的一个测试图片
|
||
const testUrl = 'https://s3.yydd.ai/gongxiangpan/由壹点AI/test-connection.txt'
|
||
const response = await fetch(testUrl, {
|
||
method: 'HEAD',
|
||
mode: 'no-cors',
|
||
cache: 'no-store'
|
||
})
|
||
|
||
const time = Date.now() - startTime
|
||
setTestResults(prev => ({
|
||
...prev,
|
||
publicS3: { success: true, time, error: '网络可达(无法确认文件是否存在)' }
|
||
}))
|
||
} catch (err) {
|
||
const time = Date.now() - startTime
|
||
setTestResults(prev => ({
|
||
...prev,
|
||
publicS3: {
|
||
success: false,
|
||
time,
|
||
error: err instanceof Error ? err.message : '连接失败'
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchNetworkStatus()
|
||
}, [])
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-100 py-8">
|
||
<div className="max-w-4xl mx-auto px-4">
|
||
<h1 className="text-2xl font-bold text-gray-800 mb-6">网络诊断</h1>
|
||
|
||
{/* 刷新按钮 */}
|
||
<div className="mb-6">
|
||
<button
|
||
onClick={fetchNetworkStatus}
|
||
disabled={loading}
|
||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
||
>
|
||
{loading ? '检测中...' : '重新检测'}
|
||
</button>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* 检测结果 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||
{/* 前端检测结果 */}
|
||
<div className="bg-white rounded-lg shadow p-6">
|
||
<h2 className="text-lg font-semibold mb-4">前端检测结果</h2>
|
||
{frontendStatus ? (
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">网络类型:</span>
|
||
<span className={`font-medium ${frontendStatus.isInternal ? 'text-green-600' : 'text-blue-600'}`}>
|
||
{frontendStatus.isInternal ? '✅ 内网' : '🌐 外网'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">当前Host:</span>
|
||
<span className="font-mono text-sm">{frontendStatus.host}</span>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="text-gray-500">加载中...</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 后端检测结果 */}
|
||
<div className="bg-white rounded-lg shadow p-6">
|
||
<h2 className="text-lg font-semibold mb-4">后端检测结果</h2>
|
||
{networkInfo ? (
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">网络类型:</span>
|
||
<span className={`font-medium ${networkInfo.isIntranet ? 'text-green-600' : 'text-blue-600'}`}>
|
||
{networkInfo.isIntranet ? '✅ 内网' : '🌐 外网'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">客户端IP:</span>
|
||
<span className="font-mono text-sm">{networkInfo.clientIp}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">IP来源:</span>
|
||
<span className="font-mono text-sm">{networkInfo.clientIpSource}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">Hostname:</span>
|
||
<span className="font-mono text-sm">{networkInfo.hostname}</span>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="text-gray-500">加载中...</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 详细诊断信息 */}
|
||
{networkInfo?.debug && (
|
||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||
<h2 className="text-lg font-semibold mb-4">详细诊断信息</h2>
|
||
<div className="space-y-3">
|
||
<div>
|
||
<span className="text-gray-600">X-Forwarded-For IP列表:</span>
|
||
<div className="font-mono text-sm bg-gray-100 p-2 rounded mt-1">
|
||
{networkInfo.debug.forwardedIpList.length > 0
|
||
? networkInfo.debug.forwardedIpList.join(' → ')
|
||
: '(空)'}
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">X-Forwarded-For中有内网IP:</span>
|
||
<span className={networkInfo.debug.hasInternalIpInForwarded ? 'text-green-600' : 'text-gray-500'}>
|
||
{networkInfo.debug.hasInternalIpInForwarded ? '是' : '否'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">X-Real-IP:</span>
|
||
<span className="font-mono text-sm">{networkInfo.debug.realIp || '(空)'}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">CF-Connecting-IP:</span>
|
||
<span className="font-mono text-sm">{networkInfo.debug.cfConnectingIp || '(空)'}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">X-Client-IP:</span>
|
||
<span className="font-mono text-sm">{networkInfo.debug.xClientIp || '(空)'}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">客户端IP是内网:</span>
|
||
<span className={networkInfo.debug.clientIpIsInternal ? 'text-green-600' : 'text-blue-600'}>
|
||
{networkInfo.debug.clientIpIsInternal === null ? '未知' : networkInfo.debug.clientIpIsInternal ? '是' : '否'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 连接测试 */}
|
||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||
<h2 className="text-lg font-semibold mb-4">连接测试</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* 内网MinIO测试 */}
|
||
<div className="border rounded p-4">
|
||
<h3 className="font-medium mb-2">内网MinIO (192.168.58.20:9000)</h3>
|
||
<button
|
||
onClick={testInternalMinIO}
|
||
className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300 text-sm mb-2"
|
||
>
|
||
测试连接
|
||
</button>
|
||
{testResults.internalMinIO && (
|
||
<div className={`text-sm ${testResults.internalMinIO.success ? 'text-green-600' : 'text-red-600'}`}>
|
||
{testResults.internalMinIO.success ? '✅' : '❌'} {testResults.internalMinIO.time}ms
|
||
{testResults.internalMinIO.error && (
|
||
<div className="text-gray-500 text-xs mt-1">{testResults.internalMinIO.error}</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 公网S3测试 */}
|
||
<div className="border rounded p-4">
|
||
<h3 className="font-medium mb-2">公网S3 (s3.yydd.ai)</h3>
|
||
<button
|
||
onClick={testPublicS3}
|
||
className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300 text-sm mb-2"
|
||
>
|
||
测试连接
|
||
</button>
|
||
{testResults.publicS3 && (
|
||
<div className={`text-sm ${testResults.publicS3.success ? 'text-green-600' : 'text-red-600'}`}>
|
||
{testResults.publicS3.success ? '✅' : '❌'} {testResults.publicS3.time}ms
|
||
{testResults.publicS3.error && (
|
||
<div className="text-gray-500 text-xs mt-1">{testResults.publicS3.error}</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 浏览器信息 */}
|
||
<div className="bg-white rounded-lg shadow p-6">
|
||
<h2 className="text-lg font-semibold mb-4">浏览器信息</h2>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">User Agent:</span>
|
||
<span className="font-mono text-xs max-w-md truncate" title={typeof navigator !== 'undefined' ? navigator.userAgent : ''}>
|
||
{typeof navigator !== 'undefined' ? navigator.userAgent : 'N/A'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">当前URL:</span>
|
||
<span className="font-mono text-xs">{typeof window !== 'undefined' ? window.location.href : 'N/A'}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">检测时间:</span>
|
||
<span className="font-mono text-xs">{networkInfo?.timestamp || 'N/A'}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 说明 */}
|
||
<div className="mt-6 text-sm text-gray-500">
|
||
<p>💡 提示:如果在内网但检测为外网,可能是以下原因:</p>
|
||
<ul className="list-disc list-inside mt-2 space-y-1">
|
||
<li>代理/VPN 导致 IP 地址被识别为公网</li>
|
||
<li>浏览器扩展修改了请求头</li>
|
||
<li>DNS 解析到了公网地址而非内网地址</li>
|
||
<li>X-Forwarded-For 头中没有内网IP</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|