From 01773500af54318460a4ba83a0f93eb6f2214cbd Mon Sep 17 00:00:00 2001 From: zqq61 <1852150449@qq.com> Date: Mon, 16 Mar 2026 00:02:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SOCKS5=E4=BB=A3=E7=90=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E3=80=81=E8=90=BD=E5=9C=B0IP=E5=9B=BD=E5=AE=B6?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E3=80=81=E8=AE=BE=E7=BD=AE=E9=A1=B5=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 代理支持SOCKS5和HTTP两种类型切换 - 落地IP查询显示国家、城市、ISP信息 - 设置页面不再隐藏已配置的值 - Airwallex API异常统一返回400+详细错误信息 --- backend/app/proxy_client.py | 30 +++++--- backend/app/routers/settings.py | 3 +- backend/app/services/airwallex_service.py | 34 ++++++++-- backend/requirements.txt | 2 +- frontend/src/pages/Settings.tsx | 83 ++++++++++++++++------- 5 files changed, 107 insertions(+), 45 deletions(-) diff --git a/backend/app/proxy_client.py b/backend/app/proxy_client.py index 531b25a..3904fc3 100644 --- a/backend/app/proxy_client.py +++ b/backend/app/proxy_client.py @@ -1,10 +1,14 @@ -"""Airwallex client with proxy support.""" +"""Airwallex client with proxy support (HTTP and SOCKS5).""" +import logging import httpx +from datetime import datetime, timezone, timedelta from airwallex.client import AirwallexClient +logger = logging.getLogger(__name__) + class ProxiedAirwallexClient(AirwallexClient): - """AirwallexClient that routes requests through an HTTP proxy.""" + """AirwallexClient that routes requests through an HTTP or SOCKS5 proxy.""" def __init__(self, proxy_url: str | None = None, **kwargs): self._proxy_url = proxy_url @@ -17,16 +21,20 @@ class ProxiedAirwallexClient(AirwallexClient): timeout=self.request_timeout, proxy=proxy_url, ) + safe_url = proxy_url.split("@")[-1] if "@" in proxy_url else proxy_url + logger.info("Airwallex client initialized with proxy: %s", safe_url) + else: + logger.info("Airwallex client initialized without proxy") def authenticate(self) -> None: """Override authenticate to use proxy for auth requests too.""" - from datetime import datetime, timezone - if self._token and self._token_expiry and datetime.now(timezone.utc) < self._token_expiry: return - # Use a proxied client for authentication - auth_kwargs = {"timeout": self.request_timeout} + logger.info("Authenticating with Airwallex API at %s (proxy: %s)", + self.auth_url, bool(self._proxy_url)) + + auth_kwargs: dict = {"timeout": self.request_timeout} if self._proxy_url: auth_kwargs["proxy"] = self._proxy_url @@ -41,11 +49,15 @@ class ProxiedAirwallexClient(AirwallexClient): }, content="{}", ) - response.raise_for_status() + + if response.status_code != 200: + body = response.text[:300] + logger.error("Auth failed: HTTP %d, body: %s", response.status_code, body) + response.raise_for_status() + data = response.json() self._token = data.get("token") - # Token valid for 30 minutes, refresh a bit early - from datetime import timedelta self._token_expiry = datetime.now(timezone.utc) + timedelta(minutes=28) + logger.info("Authentication successful, token expires in 28 minutes") finally: auth_client.close() diff --git a/backend/app/routers/settings.py b/backend/app/routers/settings.py index 1ba2a67..998b031 100644 --- a/backend/app/routers/settings.py +++ b/backend/app/routers/settings.py @@ -25,10 +25,9 @@ def get_settings( settings = db.query(SystemSetting).all() result = [] for s in settings: - value = "********" if s.key in SENSITIVE_KEYS and s.value else s.value result.append({ "key": s.key, - "value": value, + "value": s.value, "encrypted": s.encrypted, "updated_at": s.updated_at.isoformat() if s.updated_at else None, }) diff --git a/backend/app/services/airwallex_service.py b/backend/app/services/airwallex_service.py index 8630ef1..98694fd 100644 --- a/backend/app/services/airwallex_service.py +++ b/backend/app/services/airwallex_service.py @@ -23,16 +23,18 @@ def _get_setting(db: Session, key: str, default: str = "") -> str: def _build_proxy_url(db: Session) -> Optional[str]: - """Build proxy URL from settings.""" + """Build proxy URL from settings. Supports http and socks5.""" proxy_ip = _get_setting(db, "proxy_ip") proxy_port = _get_setting(db, "proxy_port") if not proxy_ip or not proxy_port: return None + proxy_type = _get_setting(db, "proxy_type", "socks5") # default socks5 + scheme = "socks5" if proxy_type == "socks5" else "http" proxy_user = _get_setting(db, "proxy_username") proxy_pass = _get_setting(db, "proxy_password") if proxy_user and proxy_pass: - return f"http://{proxy_user}:{proxy_pass}@{proxy_ip}:{proxy_port}" - return f"http://{proxy_ip}:{proxy_port}" + return f"{scheme}://{proxy_user}:{proxy_pass}@{proxy_ip}:{proxy_port}" + return f"{scheme}://{proxy_ip}:{proxy_port}" def get_client(db: Session) -> ProxiedAirwallexClient: @@ -71,7 +73,19 @@ def get_client(db: Session) -> ProxiedAirwallexClient: def ensure_authenticated(db: Session) -> ProxiedAirwallexClient: """Get client and ensure it's authenticated.""" client = get_client(db) - client.authenticate() + try: + client.authenticate() + except HTTPException: + raise + except Exception as e: + error_msg = str(e) + if hasattr(e, "response"): + try: + detail = e.response.json() + error_msg = f"Airwallex API 错误 ({e.response.status_code}): {detail.get('message', detail)}" + except Exception: + error_msg = f"Airwallex API 错误 ({e.response.status_code}): {e.response.text[:200]}" + raise HTTPException(status_code=400, detail=error_msg) return client @@ -286,7 +300,7 @@ def test_proxy(db: Session) -> Dict[str, Any]: masked = f"{scheme_user}:****@{rest}" result["proxy_url"] = masked - # Query outbound IP — with proxy if configured, otherwise direct + # Query outbound IP + country — with proxy if configured, otherwise direct for label, use_proxy in [("proxy", True), ("direct", False)]: if label == "proxy" and not proxy_url: continue @@ -295,12 +309,18 @@ def test_proxy(db: Session) -> Dict[str, Any]: if use_proxy and proxy_url: client_kwargs["proxy"] = proxy_url with httpx.Client(**client_kwargs) as client: - resp = client.get("https://api.ipify.org?format=json") + # Use ip-api.com for IP + country info + resp = client.get("http://ip-api.com/json/?fields=query,country,countryCode,city,isp") ip_data = resp.json() - result[f"{label}_ip"] = ip_data.get("ip", "unknown") + result[f"{label}_ip"] = ip_data.get("query", "unknown") + result[f"{label}_country"] = ip_data.get("country", "unknown") + result[f"{label}_country_code"] = ip_data.get("countryCode", "") + result[f"{label}_city"] = ip_data.get("city", "") + result[f"{label}_isp"] = ip_data.get("isp", "") result[f"{label}_status"] = "ok" except Exception as e: result[f"{label}_ip"] = None + result[f"{label}_country"] = None result[f"{label}_status"] = f"failed: {str(e)[:150]}" result["success"] = result.get("proxy_status", result.get("direct_status")) == "ok" diff --git a/backend/requirements.txt b/backend/requirements.txt index 90e2c5c..beb2774 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,6 +7,6 @@ bcrypt==4.1.3 python-dotenv==1.0.1 pydantic==2.11.3 pydantic-settings==2.6.0 -httpx==0.28.1 +httpx[socks]==0.28.1 cryptography==43.0.0 email-validator==2.1.1 diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 03c75a4..1908961 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,15 +1,23 @@ import { useState, useEffect } from 'react' -import { Card, Form, Input, InputNumber, Button, Divider, message, Space, Descriptions, Tag } from 'antd' +import { Card, Form, Input, InputNumber, Button, Divider, message, Space, Descriptions, Tag, Select } from 'antd' import { SaveOutlined, ApiOutlined, GlobalOutlined } from '@ant-design/icons' -import { settingsApi } from '@/services/api' +import { settingsApi, getErrorMsg } from '@/services/api' interface ProxyTestResult { success: boolean proxy_configured: boolean proxy_url: string | null proxy_ip: string | null + proxy_country: string | null + proxy_country_code: string | null + proxy_city: string | null + proxy_isp: string | null proxy_status: string | null direct_ip: string | null + direct_country: string | null + direct_country_code: string | null + direct_city: string | null + direct_isp: string | null direct_status: string | null } @@ -28,14 +36,12 @@ export default function Settings() { const data: Record = {} if (Array.isArray(res.data)) { for (const item of res.data as { key: string; value: string }[]) { - // Don't fill masked values into password fields - if (item.value === '********') continue data[item.key] = item.value } } form.setFieldsValue(data) - } catch { - message.error('获取设置失败') + } catch (err) { + message.error(getErrorMsg(err, '获取设置失败')) } finally { setLoading(false) } @@ -55,8 +61,8 @@ export default function Settings() { .map(([key, value]) => ({ key, value: String(value) })) await settingsApi.updateSettings(updates) message.success('设置已保存') - } catch { - message.error('保存设置失败') + } catch (err) { + message.error(getErrorMsg(err, '保存设置失败')) } finally { setSaving(false) } @@ -71,8 +77,8 @@ export default function Settings() { } else { message.error(res.data.message || '连接测试失败') } - } catch { - message.error('连接测试失败') + } catch (err) { + message.error(getErrorMsg(err, '连接测试失败')) } finally { setTesting(false) } @@ -89,13 +95,42 @@ export default function Settings() { } else { message.warning('代理测试失败,请检查配置') } - } catch { - message.error('代理测试请求失败') + } catch (err) { + message.error(getErrorMsg(err, '代理测试请求失败')) } finally { setTestingProxy(false) } } + const renderIpInfo = (label: string, prefix: string) => { + if (!proxyResult) return null + const ip = proxyResult[`${prefix}_ip` as keyof ProxyTestResult] as string | null + const country = proxyResult[`${prefix}_country` as keyof ProxyTestResult] as string | null + const countryCode = proxyResult[`${prefix}_country_code` as keyof ProxyTestResult] as string | null + const city = proxyResult[`${prefix}_city` as keyof ProxyTestResult] as string | null + const isp = proxyResult[`${prefix}_isp` as keyof ProxyTestResult] as string | null + const status = proxyResult[`${prefix}_status` as keyof ProxyTestResult] as string | null + + if (status !== 'ok') { + return ( + + {status} + + ) + } + + return ( + + + IP: {ip} + 国家: {country} ({countryCode}) + {city && 城市: {city}} + {isp && ISP: {isp}} + + + ) + } + return (
@@ -115,7 +150,7 @@ export default function Settings() { name="airwallex_api_key" rules={[{ required: true, message: '请输入 API Key' }]} > - + @@ -130,6 +165,12 @@ export default function Settings() { + + + @@ -140,7 +181,7 @@ export default function Settings() { - +