焕新网络/系统页 UI,并加入基于出口的分流诊断与 Nomadro 场景推荐。
简化分流文案与手动检测交互,系统页对齐管线/芯片/ScoreRing 设计语言;README 同步更新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
144
web2_network.py
144
web2_network.py
@@ -30,12 +30,18 @@ DOMESTIC_UPLOAD_URLS = (
|
||||
"https://www.163.com/",
|
||||
)
|
||||
|
||||
IP_API_TEMPLATE = "http://ip-api.com/json/{ip}?fields=status,country,countryCode,message,query"
|
||||
IP_API_TEMPLATE = "http://ip-api.com/json/{ip}?fields=status,country,countryCode,regionName,city,isp,org,as,asname,message,query"
|
||||
IP_API_SELF = (
|
||||
"http://ip-api.com/json/?fields=status,message,query,country,countryCode,"
|
||||
"regionName,city,isp,org,as,asname,proxy,hosting,mobile"
|
||||
)
|
||||
IPIFY_URL = "https://api.ipify.org?format=json"
|
||||
|
||||
DOWNLOAD_CAP_BYTES = 600_000
|
||||
UPLOAD_BYTES = 256 * 1024
|
||||
CONNECT_TIMEOUT = 8.0
|
||||
READ_TIMEOUT = 25.0
|
||||
NETWORK_LOGIC_VERSION = 2
|
||||
|
||||
|
||||
def _is_probably_china_country(code: Optional[str]) -> bool:
|
||||
@@ -59,28 +65,109 @@ async def _resolve_ipv4(host: str) -> tuple[Optional[str], Optional[str]]:
|
||||
return await asyncio.to_thread(_sync)
|
||||
|
||||
|
||||
async def _country_for_ip(client: httpx.AsyncClient, ip: str) -> dict[str, Any]:
|
||||
if not ip:
|
||||
return {"country": None, "country_code": None, "error": "no ip"}
|
||||
async def _geo_for_ip(client: httpx.AsyncClient, ip: Optional[str] = None) -> dict[str, Any]:
|
||||
url = IP_API_SELF if not ip else IP_API_TEMPLATE.format(ip=ip)
|
||||
try:
|
||||
r = await client.get(IP_API_TEMPLATE.format(ip=ip), timeout=6.0)
|
||||
r = await client.get(url, timeout=8.0)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
except Exception as e:
|
||||
return {"country": None, "country_code": None, "error": str(e)}
|
||||
return {"error": str(e)}
|
||||
if data.get("status") != "success":
|
||||
return {
|
||||
"country": None,
|
||||
"country_code": None,
|
||||
"error": data.get("message") or "lookup failed",
|
||||
"ip": ip or data.get("query"),
|
||||
}
|
||||
return {
|
||||
"ip": data.get("query") or ip,
|
||||
"country": data.get("country"),
|
||||
"country_code": data.get("countryCode"),
|
||||
"region": data.get("regionName"),
|
||||
"city": data.get("city"),
|
||||
"isp": data.get("isp"),
|
||||
"org": data.get("org"),
|
||||
"asn": data.get("as"),
|
||||
"asname": data.get("asname"),
|
||||
"error": None,
|
||||
}
|
||||
|
||||
|
||||
async def _country_for_ip(client: httpx.AsyncClient, ip: str) -> dict[str, Any]:
|
||||
geo = await _geo_for_ip(client, ip)
|
||||
return {
|
||||
"country": geo.get("country"),
|
||||
"country_code": geo.get("country_code"),
|
||||
"error": geo.get("error"),
|
||||
}
|
||||
|
||||
|
||||
async def _probe_default_egress(client: httpx.AsyncClient) -> dict[str, Any]:
|
||||
"""本机默认出口:优先 ip-api 直连,失败则用 ipify + 反查。"""
|
||||
geo = await _geo_for_ip(client, None)
|
||||
if geo.get("ip") and not geo.get("error"):
|
||||
return geo
|
||||
fallback = await _sample_overseas_ip(client)
|
||||
if fallback.get("ip") and not fallback.get("error"):
|
||||
return fallback
|
||||
return geo if geo.get("ip") else fallback
|
||||
|
||||
|
||||
async def _sample_overseas_ip(client: httpx.AsyncClient) -> dict[str, Any]:
|
||||
try:
|
||||
r = await client.get(IPIFY_URL, timeout=8.0)
|
||||
r.raise_for_status()
|
||||
ip = (r.json().get("ip") or "").strip()
|
||||
if not ip:
|
||||
return {"error": "empty ip"}
|
||||
return await _geo_for_ip(client, ip)
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _format_isp_line(geo: dict[str, Any]) -> str:
|
||||
isp = (geo.get("isp") or geo.get("org") or "").strip()
|
||||
country = (geo.get("country") or "").strip()
|
||||
city = (geo.get("city") or "").strip()
|
||||
loc = " · ".join(x for x in (country, city) if x)
|
||||
if isp and loc:
|
||||
return f"{isp}({loc})"
|
||||
return isp or loc or "未知运营商"
|
||||
|
||||
|
||||
def _compute_routes(
|
||||
egress: dict[str, Any],
|
||||
overseas: dict[str, Any],
|
||||
*,
|
||||
google_ok: bool,
|
||||
douyin_ok: bool,
|
||||
) -> tuple[Optional[bool], str]:
|
||||
ip = egress.get("ip")
|
||||
o_ip = overseas.get("ip")
|
||||
cc = (egress.get("country_code") or "").upper()
|
||||
|
||||
if egress.get("error") and not ip:
|
||||
if google_ok and douyin_ok:
|
||||
return None, "运营商未识别"
|
||||
return None, "出口未识别,请重检"
|
||||
|
||||
if ip and o_ip and ip != o_ip:
|
||||
return True, "已分流"
|
||||
|
||||
cn = _is_probably_china_country(cc)
|
||||
if google_ok and douyin_ok:
|
||||
if cn:
|
||||
return True, "已分流"
|
||||
return False, "未分流"
|
||||
|
||||
if douyin_ok and not google_ok:
|
||||
return False, "Google 不通"
|
||||
|
||||
if google_ok and not douyin_ok:
|
||||
return False, "抖音异常"
|
||||
|
||||
return False, "均不可达"
|
||||
|
||||
|
||||
async def _latency_get(client: httpx.AsyncClient, url: str) -> tuple[bool, float, Optional[str]]:
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
@@ -147,7 +234,7 @@ async def _try_domestic_upload(client: httpx.AsyncClient) -> tuple[Optional[floa
|
||||
async def run_network_dashboard() -> dict[str, Any]:
|
||||
"""
|
||||
返回结构供前端仪表盘使用。
|
||||
routes_different:基于解析 IP 的国家码;若 API 失败则给 likely / unknown。
|
||||
routes_different:基于本机出口 IP/运营商 + Google/抖音可达性判断,不再用 CDN 解析 IP 比国家。
|
||||
"""
|
||||
timeout = httpx.Timeout(READ_TIMEOUT, connect=CONNECT_TIMEOUT)
|
||||
limits = httpx.Limits(max_keepalive_connections=4, max_connections=8)
|
||||
@@ -170,42 +257,43 @@ async def run_network_dashboard() -> dict[str, Any]:
|
||||
|
||||
g_country_task = _country_for_ip(client, g_ip) if g_ip else _empty_geo()
|
||||
d_country_task = _country_for_ip(client, d_ip) if d_ip else _empty_geo()
|
||||
egress_task = _probe_default_egress(client)
|
||||
overseas_task = _sample_overseas_ip(client)
|
||||
|
||||
(
|
||||
(g_ok, g_ms, g_err),
|
||||
(d_ok, d_ms, d_err),
|
||||
g_geo,
|
||||
d_geo,
|
||||
egress,
|
||||
overseas,
|
||||
) = await asyncio.gather(
|
||||
_latency_get(client, GOOGLE_CHECK_URL),
|
||||
_latency_get(client, DOUYIN_CHECK_URL),
|
||||
g_country_task,
|
||||
d_country_task,
|
||||
egress_task,
|
||||
overseas_task,
|
||||
)
|
||||
|
||||
if not isinstance(g_geo, dict):
|
||||
g_geo = {"country": None, "country_code": None, "error": None}
|
||||
if not isinstance(d_geo, dict):
|
||||
d_geo = {"country": None, "country_code": None, "error": None}
|
||||
if not isinstance(egress, dict):
|
||||
egress = {"error": "egress probe failed"}
|
||||
if not isinstance(overseas, dict):
|
||||
overseas = {"error": "overseas probe failed"}
|
||||
|
||||
g_code = g_geo.get("country_code")
|
||||
d_code = d_geo.get("country_code")
|
||||
|
||||
routes_different: Optional[bool] = None
|
||||
routes_note = ""
|
||||
if g_code and d_code:
|
||||
routes_different = _is_probably_china_country(g_code) != _is_probably_china_country(d_code)
|
||||
routes_note = "依据解析 IP 的国家码:海外站点与国内站点出口不一致" if routes_different else (
|
||||
"依据解析 IP:两国别相同或未区分境内外(若您期望分流,请检查代理/路由)"
|
||||
)
|
||||
else:
|
||||
routes_note = "未能查询到双方 IP 国家,无法严格判断线路是否分流"
|
||||
if g_ip and d_ip and g_ip != d_ip:
|
||||
routes_note += ";两站点解析 IP 不同,通常走不同目标。"
|
||||
if g_res_err:
|
||||
routes_note += f" Google DNS: {g_res_err}"
|
||||
if d_res_err:
|
||||
routes_note += f" 抖音 DNS: {d_res_err}"
|
||||
routes_different, routes_note = _compute_routes(
|
||||
egress,
|
||||
overseas,
|
||||
google_ok=g_ok,
|
||||
douyin_ok=d_ok,
|
||||
)
|
||||
|
||||
# 测速(并行)
|
||||
od_mbps, od_err, od_bytes = await _measure_download_mbps(client, GOOGLE_DOWNLOAD_URL)
|
||||
@@ -238,6 +326,13 @@ async def run_network_dashboard() -> dict[str, Any]:
|
||||
"routes": {
|
||||
"different": routes_different,
|
||||
"note": routes_note.strip(),
|
||||
"isp": egress.get("isp") or egress.get("org"),
|
||||
"country": egress.get("country"),
|
||||
"country_code": egress.get("country_code"),
|
||||
},
|
||||
"egress": {
|
||||
"default": egress,
|
||||
"overseas_sample": overseas,
|
||||
},
|
||||
"speed": {
|
||||
"overseas": {
|
||||
@@ -263,5 +358,6 @@ async def run_network_dashboard() -> dict[str, Any]:
|
||||
"at": time.time(),
|
||||
"download_cap_bytes": DOWNLOAD_CAP_BYTES,
|
||||
"upload_bytes": UPLOAD_BYTES,
|
||||
"logic_version": NETWORK_LOGIC_VERSION,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user