From eb8d43841cdb6851a8bddf569e96429a0994280d Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 19:38:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A3=80=E6=B5=8B=E4=B8=8D=E6=AD=A3=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 116 +++++++++++++++++++++++++--------------------- public/index.html | 9 +++- 2 files changed, 71 insertions(+), 54 deletions(-) diff --git a/backend/main.py b/backend/main.py index 03af53f..cf3b0e4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -421,7 +421,42 @@ PROXY_CHECK_URL = os.getenv("PROXY_CHECK_URL", "https://httpbin.org/ip") @app.get("/api/check-proxy") async def api_check_proxy(proxy: Optional[str] = Query(None, description="可选,指定要检测的代理 URL(不传则用环境变量/隧道/KDL)")): - """检测代理是否可用:用解析到的代理请求测试页,返回是否成功及来源。""" + """检测代理是否可用:返回连通状态、出口 IP(若可解析)与错误原因。""" + def _extract_origin_ip(resp: httpx.Response) -> Optional[str]: + """优先解析 httpbin /ip 的 origin 字段,解析失败返回 None。""" + try: + data = resp.json() + if isinstance(data, dict): + origin = (data.get("origin") or "").strip() + return origin or None + except Exception: + pass + return None + + def _format_proxy_result(ok: bool, *, source: str, preview: str, status_code: Optional[int] = None, + origin_ip: Optional[str] = None, error: Optional[str] = None, note: Optional[str] = None) -> dict: + status = "available" if ok else "unavailable" + out = { + "ok": ok, + "status": status, + "source": source, + "proxy_preview": preview, + "check_url": PROXY_CHECK_URL, + } + if status_code is not None: + out["status_code"] = status_code + if origin_ip: + out["origin_ip"] = origin_ip + if note: + out["note"] = note + if error: + out["error"] = error + # 常见 socks 认证失败给出更可执行的提示 + if "User was rejected by the SOCKS5 server" in error: + out["reason"] = "proxy_auth_rejected" + out["suggestion"] = "请检查代理用户名/密码、端口、以及代理服务商白名单配置" + return out + proxy_url = (proxy or "").strip() source = "query" if not proxy_url: @@ -439,11 +474,12 @@ async def api_check_proxy(proxy: Optional[str] = Query(None, description="可选 if proxy_url: logger.info("check-proxy: using env/body, len=%s", len(proxy_url)) if not proxy_url: - return { - "ok": False, - "source": "none", - "error": "未配置代理。请填写代理、或设置 HTTP_PROXY/HTTPS_PROXY、或配置 TUNNEL_PROXY(固定隧道)、或 KDL 代理 API。", - } + return _format_proxy_result( + False, + source="none", + preview="(empty)", + error="未配置代理。请填写代理、或设置 HTTP_PROXY/HTTPS_PROXY、或配置 TUNNEL_PROXY(固定隧道)、或 KDL 代理 API。", + ) # 脱敏显示(不暴露密码) def _preview(u: str) -> str: if not u or "@" not in u: @@ -475,66 +511,42 @@ async def api_check_proxy(proxy: Optional[str] = Query(None, description="可选 async with httpx.AsyncClient(trust_env=False, timeout=15.0, transport=transport) as client: resp = await client.get(PROXY_CHECK_URL) if resp.status_code == 200: + origin_ip = _extract_origin_ip(resp) logger.info("check-proxy: ok (socks), status=%s", resp.status_code) - return { - "ok": True, - "source": source, - "proxy_preview": preview, - "check_url": PROXY_CHECK_URL, - "status_code": resp.status_code, - } + return _format_proxy_result( + True, source=source, preview=preview, status_code=resp.status_code, origin_ip=origin_ip + ) logger.warning("check-proxy: fail (socks), status=%s", resp.status_code) - return { - "ok": False, - "source": source, - "proxy_preview": preview, - "error": f"请求测试页返回 {resp.status_code}", - "status_code": resp.status_code, - } + return _format_proxy_result( + False, source=source, preview=preview, status_code=resp.status_code, + error=f"请求测试页返回 {resp.status_code}" + ) except ImportError: logger.info("check-proxy: socks5 已配置,跳过连通性检测(需 pip install httpx-socks 方可检测)") - return { - "ok": True, - "source": source, - "proxy_preview": preview, - "note": "socks5 代理已配置;连通性检测需安装 pip install httpx-socks", - } + return _format_proxy_result( + True, source=source, preview=preview, + note="socks5 代理已配置;连通性检测需安装 pip install httpx-socks" + ) except Exception as e: logger.warning("check-proxy: socks exception %s", e) - return { - "ok": False, - "source": source, - "proxy_preview": preview, - "error": str(e), - } + return _format_proxy_result(False, source=source, preview=preview, error=str(e)) try: async with httpx.AsyncClient(trust_env=False, timeout=15.0, proxy=proxy_url) as client: resp = await client.get(PROXY_CHECK_URL) if resp.status_code == 200: + origin_ip = _extract_origin_ip(resp) logger.info("check-proxy: ok, status=%s", resp.status_code) - return { - "ok": True, - "source": source, - "proxy_preview": preview, - "check_url": PROXY_CHECK_URL, - "status_code": resp.status_code, - } + return _format_proxy_result( + True, source=source, preview=preview, status_code=resp.status_code, origin_ip=origin_ip + ) logger.warning("check-proxy: fail, status=%s", resp.status_code) - return { - "ok": False, - "source": source, - "proxy_preview": preview, - "error": f"请求测试页返回 {resp.status_code}", - "status_code": resp.status_code, - } + return _format_proxy_result( + False, source=source, preview=preview, status_code=resp.status_code, + error=f"请求测试页返回 {resp.status_code}" + ) except Exception as e: logger.warning("check-proxy: exception %s", e) - return { - "ok": False, - "source": source, - "proxy_preview": preview, - "error": str(e), - } + return _format_proxy_result(False, source=source, preview=preview, error=str(e)) def _proxy_from_env() -> str: diff --git a/public/index.html b/public/index.html index dacc75b..1818f3b 100644 --- a/public/index.html +++ b/public/index.html @@ -1050,9 +1050,14 @@ try { var data = await callApi(url); if (data && data.ok) { - log('代理正常。来源: ' + (data.source || '') + (data.proxy_preview ? ',代理: ' + data.proxy_preview : ''), 'ok'); + var msg = '代理可用。状态: ' + (data.status || 'available') + ',来源: ' + (data.source || ''); + if (data.origin_ip) msg += ',出口IP: ' + data.origin_ip; + if (data.proxy_preview) msg += ',代理: ' + data.proxy_preview; + log(msg, 'ok'); } else { - log('代理不可用: ' + (data && data.error ? data.error : JSON.stringify(data)), 'error'); + var err = data && data.error ? data.error : JSON.stringify(data); + var detail = data && data.reason ? (',原因: ' + data.reason) : ''; + log('代理不可用: ' + err + detail, 'error'); } } catch (e) { log('检测代理失败: ' + (e.message || e), 'error');