mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor(proxy-node): 统一代理解析,全面支持 tunnel 模式
新增 resolve_ops_proxy_config 合并 proxy 和 tunnel_node_id 的解析, 避免各架构重复调用 _resolve_effective_node。所有架构连接器 (anyrouter/nekocode/sub2api/yescode) 和 HTTPClientPool 均适配 tunnel 模式,通过 TunnelTransport 替代传统代理。
This commit is contained in:
@@ -204,6 +204,13 @@ class HTTPClientPool:
|
||||
if not proxy_config:
|
||||
proxy_config = get_system_proxy_config()
|
||||
|
||||
# tunnel 模式检查:tunnel 节点走专用的 TunnelTransport 客户端
|
||||
from src.services.proxy_node.resolver import resolve_delegate_config
|
||||
|
||||
delegate_cfg = resolve_delegate_config(proxy_config)
|
||||
if delegate_cfg and delegate_cfg.get("tunnel"):
|
||||
return await cls._get_tunnel_client(delegate_cfg["node_id"])
|
||||
|
||||
cache_key = compute_proxy_cache_key(proxy_config)
|
||||
|
||||
# 无代理时返回默认客户端
|
||||
|
||||
@@ -188,7 +188,10 @@ def _parse_session_user_id(cookie_input: str) -> tuple[str | None, str | None]:
|
||||
|
||||
|
||||
async def _get_acw_cookie(
|
||||
base_url: str, timeout: float = 10, proxy: str | httpx.Proxy | None = None
|
||||
base_url: str,
|
||||
timeout: float = 10,
|
||||
proxy: str | httpx.Proxy | None = None,
|
||||
tunnel_node_id: str | None = None,
|
||||
) -> str | None:
|
||||
"""
|
||||
获取 acw_sc__v2 Cookie
|
||||
@@ -199,6 +202,7 @@ async def _get_acw_cookie(
|
||||
base_url: 目标站点 URL
|
||||
timeout: 请求超时时间
|
||||
proxy: 代理地址
|
||||
tunnel_node_id: tunnel 模式节点 ID(优先于 proxy)
|
||||
|
||||
Returns:
|
||||
Cookie 字符串 (acw_sc__v2=xxx),如果不需要或获取失败则返回 None
|
||||
@@ -209,7 +213,11 @@ async def _get_acw_cookie(
|
||||
"timeout": timeout,
|
||||
"verify": get_ssl_context(),
|
||||
}
|
||||
if proxy:
|
||||
if tunnel_node_id:
|
||||
from src.services.proxy_node.tunnel_transport import TunnelTransport
|
||||
|
||||
client_kwargs["transport"] = TunnelTransport(tunnel_node_id, timeout=timeout)
|
||||
elif proxy:
|
||||
client_kwargs["proxy"] = proxy
|
||||
logger.debug(f"获取 acw_sc__v2 Cookie 使用代理: {proxy}")
|
||||
|
||||
@@ -273,7 +281,9 @@ class AnyrouterConnector(ProviderConnector):
|
||||
self._user_id, _ = _parse_session_user_id(session_cookie)
|
||||
|
||||
# 尝试获取反爬 Cookie(使用配置中的代理)
|
||||
self._acw_cookie = await _get_acw_cookie(self.base_url, proxy=self._proxy)
|
||||
self._acw_cookie = await _get_acw_cookie(
|
||||
self.base_url, proxy=self._proxy, tunnel_node_id=self._tunnel_node_id
|
||||
)
|
||||
|
||||
self._set_connected()
|
||||
return True
|
||||
@@ -406,11 +416,11 @@ class AnyrouterArchitecture(ProviderArchitecture):
|
||||
Returns:
|
||||
包含 acw_cookie 的配置
|
||||
"""
|
||||
# 从 config 获取代理配置(支持 proxy_node_id 和旧的 proxy URL)
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy
|
||||
# 从 config 获取代理配置(支持 proxy_node_id、tunnel 和旧的 proxy URL)
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy_config
|
||||
|
||||
proxy = resolve_ops_proxy(config)
|
||||
acw_cookie = await _get_acw_cookie(base_url, proxy=proxy)
|
||||
proxy, tunnel_node_id = resolve_ops_proxy_config(config)
|
||||
acw_cookie = await _get_acw_cookie(base_url, proxy=proxy, tunnel_node_id=tunnel_node_id)
|
||||
if acw_cookie:
|
||||
return {"acw_cookie": acw_cookie}
|
||||
return {}
|
||||
|
||||
@@ -50,11 +50,12 @@ class ProviderConnector(ABC):
|
||||
self._expires_at: datetime | None = None
|
||||
self._last_error: str | None = None
|
||||
|
||||
# 代理配置(支持 proxy_node_id 和旧的 proxy URL)
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy, resolve_ops_tunnel_node_id
|
||||
# 代理配置(支持 proxy_node_id、tunnel 和旧的 proxy URL)
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy_config
|
||||
|
||||
self._proxy: str | httpx.Proxy | None = resolve_ops_proxy(self.config)
|
||||
self._tunnel_node_id: str | None = resolve_ops_tunnel_node_id(self.config)
|
||||
self._proxy: str | httpx.Proxy | None
|
||||
self._tunnel_node_id: str | None
|
||||
self._proxy, self._tunnel_node_id = resolve_ops_proxy_config(self.config)
|
||||
|
||||
# HTTP 客户端配置
|
||||
self._timeout = self.config.get("timeout", 30)
|
||||
|
||||
@@ -193,10 +193,14 @@ class NekoCodeArchitecture(ProviderArchitecture):
|
||||
"timeout": 10,
|
||||
"verify": get_ssl_context(),
|
||||
}
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy_config
|
||||
|
||||
proxy = resolve_ops_proxy(config)
|
||||
if proxy:
|
||||
proxy, tunnel_node_id = resolve_ops_proxy_config(config)
|
||||
if tunnel_node_id:
|
||||
from src.services.proxy_node.tunnel_transport import TunnelTransport
|
||||
|
||||
client_kwargs["transport"] = TunnelTransport(tunnel_node_id, timeout=10.0)
|
||||
elif proxy:
|
||||
client_kwargs["proxy"] = proxy
|
||||
|
||||
async with httpx.AsyncClient(**client_kwargs) as client:
|
||||
|
||||
@@ -99,12 +99,17 @@ class _Sub2ApiTokenMixin:
|
||||
base_url: str
|
||||
_timeout: int | float
|
||||
_proxy: str | httpx.Proxy | None
|
||||
_tunnel_node_id: str | None
|
||||
|
||||
@asynccontextmanager
|
||||
async def _get_raw_client(self) -> AsyncIterator[httpx.AsyncClient]:
|
||||
"""获取不带 auth hook 的裸 HTTP 客户端(用于登录/刷新 token)"""
|
||||
transport = None
|
||||
if self._proxy:
|
||||
if self._tunnel_node_id:
|
||||
from src.services.proxy_node.tunnel_transport import TunnelTransport
|
||||
|
||||
transport = TunnelTransport(self._tunnel_node_id, timeout=self._timeout)
|
||||
elif self._proxy:
|
||||
transport = httpx.AsyncHTTPTransport(proxy=self._proxy)
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
@@ -474,15 +479,19 @@ class Sub2ApiArchitecture(ProviderArchitecture):
|
||||
"""
|
||||
base_url = base_url.rstrip("/")
|
||||
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy_config
|
||||
|
||||
proxy = resolve_ops_proxy(config)
|
||||
proxy, tunnel_node_id = resolve_ops_proxy_config(config)
|
||||
client_kwargs: dict[str, Any] = {
|
||||
"base_url": base_url,
|
||||
"timeout": 30.0,
|
||||
"verify": get_ssl_context(),
|
||||
}
|
||||
if proxy:
|
||||
if tunnel_node_id:
|
||||
from src.services.proxy_node.tunnel_transport import TunnelTransport
|
||||
|
||||
client_kwargs["transport"] = TunnelTransport(tunnel_node_id, timeout=30.0)
|
||||
elif proxy:
|
||||
client_kwargs["proxy"] = proxy
|
||||
|
||||
email = credentials.get("email", "").strip()
|
||||
|
||||
@@ -232,10 +232,10 @@ class YesCodeArchitecture(ProviderArchitecture):
|
||||
|
||||
cookie_header = _build_cookie_header(cookie_input)
|
||||
|
||||
# 获取代理配置(支持 proxy_node_id 和旧的 proxy URL)
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy
|
||||
# 获取代理配置(支持 proxy_node_id、tunnel 和旧的 proxy URL)
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy_config
|
||||
|
||||
proxy = resolve_ops_proxy(config)
|
||||
proxy, tunnel_node_id = resolve_ops_proxy_config(config)
|
||||
|
||||
try:
|
||||
# 构建 client 参数
|
||||
@@ -244,7 +244,11 @@ class YesCodeArchitecture(ProviderArchitecture):
|
||||
"timeout": 10.0,
|
||||
"verify": get_ssl_context(),
|
||||
}
|
||||
if proxy:
|
||||
if tunnel_node_id:
|
||||
from src.services.proxy_node.tunnel_transport import TunnelTransport
|
||||
|
||||
client_kwargs["transport"] = TunnelTransport(tunnel_node_id, timeout=10.0)
|
||||
elif proxy:
|
||||
client_kwargs["proxy"] = proxy
|
||||
|
||||
# 创建临时 client 获取合并数据
|
||||
|
||||
@@ -1034,10 +1034,9 @@ class ProviderOpsService:
|
||||
)
|
||||
|
||||
# 获取代理配置(支持 proxy_node_id、tunnel 模式和旧的 proxy URL)
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy, resolve_ops_tunnel_node_id
|
||||
from src.services.proxy_node.resolver import resolve_ops_proxy_config
|
||||
|
||||
proxy = resolve_ops_proxy(config)
|
||||
tunnel_node_id = resolve_ops_tunnel_node_id(config)
|
||||
proxy, tunnel_node_id = resolve_ops_proxy_config(config)
|
||||
|
||||
try:
|
||||
# 构建 httpx client 参数
|
||||
|
||||
@@ -14,6 +14,7 @@ from .resolver import (
|
||||
make_proxy_param,
|
||||
resolve_delegate_config,
|
||||
resolve_ops_proxy,
|
||||
resolve_ops_proxy_config,
|
||||
resolve_ops_tunnel_node_id,
|
||||
resolve_proxy_info,
|
||||
)
|
||||
@@ -36,6 +37,7 @@ __all__ = [
|
||||
"invalidate_system_proxy_cache",
|
||||
"resolve_delegate_config",
|
||||
"resolve_ops_proxy",
|
||||
"resolve_ops_proxy_config",
|
||||
"resolve_ops_tunnel_node_id",
|
||||
"resolve_proxy_info",
|
||||
]
|
||||
|
||||
@@ -216,63 +216,67 @@ def _resolve_effective_node(
|
||||
return None, None
|
||||
|
||||
|
||||
def resolve_ops_proxy(
|
||||
def resolve_ops_proxy_config(
|
||||
connector_config: dict[str, Any] | None,
|
||||
) -> str | httpx.Proxy | None:
|
||||
) -> tuple[str | httpx.Proxy | None, str | None]:
|
||||
"""
|
||||
从 ops connector.config 中解析代理参数(含系统默认回退)
|
||||
一次解析 ops connector 的代理参数和 tunnel 节点 ID
|
||||
|
||||
合并 resolve_ops_proxy + resolve_ops_tunnel_node_id,避免重复调用
|
||||
_resolve_effective_node。两个返回值互斥:tunnel 模式时 proxy 为 None,
|
||||
非 tunnel 模式时 tunnel_node_id 为 None。
|
||||
|
||||
优先级:
|
||||
1. connector_config.proxy_node_id(新格式)
|
||||
2. connector_config.proxy(旧格式 URL 字符串)
|
||||
3. 系统默认代理节点
|
||||
|
||||
tunnel 模式节点不返回代理 URL(由 resolve_ops_tunnel_node_id 处理)。
|
||||
|
||||
Args:
|
||||
connector_config: connector 的 config 字典
|
||||
|
||||
Returns:
|
||||
httpx 可接受的代理参数(str 或 httpx.Proxy),或 None
|
||||
(proxy, tunnel_node_id)
|
||||
"""
|
||||
from .tunnel_transport import is_tunnel_node
|
||||
|
||||
node_id, node_info = _resolve_effective_node(connector_config)
|
||||
if node_id and node_info:
|
||||
if is_tunnel_node(node_info):
|
||||
return None # tunnel 模式不使用 proxy URL
|
||||
return None, node_id
|
||||
try:
|
||||
url = build_proxy_url({"node_id": node_id, "enabled": True})
|
||||
return make_proxy_param(url)
|
||||
return make_proxy_param(url), None
|
||||
except Exception as exc:
|
||||
logger.warning("解析 proxy_node_id={} 失败,回退到直连: {}", node_id, exc)
|
||||
return None
|
||||
return None, None
|
||||
|
||||
# 旧格式:直接返回 proxy URL 字符串
|
||||
if connector_config:
|
||||
proxy = connector_config.get("proxy")
|
||||
if isinstance(proxy, str) and proxy.strip():
|
||||
return proxy
|
||||
return proxy, None
|
||||
|
||||
return None
|
||||
return None, None
|
||||
|
||||
|
||||
def resolve_ops_proxy(
|
||||
connector_config: dict[str, Any] | None,
|
||||
) -> str | httpx.Proxy | None:
|
||||
"""从 ops connector.config 中解析代理参数(含系统默认回退)
|
||||
|
||||
tunnel 模式节点不返回代理 URL。
|
||||
如需同时获取 tunnel_node_id,请使用 resolve_ops_proxy_config 避免重复解析。
|
||||
"""
|
||||
proxy, _ = resolve_ops_proxy_config(connector_config)
|
||||
return proxy
|
||||
|
||||
|
||||
def resolve_ops_tunnel_node_id(
|
||||
connector_config: dict[str, Any] | None,
|
||||
) -> str | None:
|
||||
"""解析 ops connector 的 tunnel 节点 ID
|
||||
|
||||
如需同时获取 proxy,请使用 resolve_ops_proxy_config 避免重复解析。
|
||||
"""
|
||||
解析 ops connector 的 tunnel 节点 ID
|
||||
|
||||
如果配置的代理节点是 tunnel 模式且已连接,返回 node_id。
|
||||
否则返回 None(含系统默认代理回退)。
|
||||
"""
|
||||
from .tunnel_transport import is_tunnel_node
|
||||
|
||||
node_id, node_info = _resolve_effective_node(connector_config)
|
||||
if node_id and node_info and is_tunnel_node(node_info):
|
||||
return node_id
|
||||
|
||||
return None
|
||||
_, tunnel_node_id = resolve_ops_proxy_config(connector_config)
|
||||
return tunnel_node_id
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -353,6 +357,16 @@ def build_proxy_client_kwargs(
|
||||
verify = get_ssl_context()
|
||||
|
||||
kwargs: dict[str, Any] = {"timeout": timeout, "verify": verify, **extra}
|
||||
|
||||
# tunnel 模式优先:当代理节点为 tunnel 模式时,使用 TunnelTransport
|
||||
delegate_cfg = resolve_delegate_config(proxy_config)
|
||||
if delegate_cfg and delegate_cfg.get("tunnel"):
|
||||
from src.services.proxy_node.tunnel_transport import TunnelTransport
|
||||
|
||||
timeout_secs = timeout if isinstance(timeout, (int, float)) else 60.0
|
||||
kwargs["transport"] = TunnelTransport(delegate_cfg["node_id"], timeout=timeout_secs)
|
||||
return kwargs
|
||||
|
||||
proxy_param = resolve_proxy_param(proxy_config)
|
||||
if proxy_param:
|
||||
kwargs["proxy"] = proxy_param
|
||||
|
||||
Reference in New Issue
Block a user