fix: 新增代理

This commit is contained in:
丹尼尔
2026-03-12 13:02:25 +08:00
parent bdba4ec071
commit 66362780a0
8 changed files with 3146 additions and 69 deletions

View File

@@ -381,6 +381,84 @@ async def api_ws_status() -> dict:
return {"connected": is_ws_connected()}
# 代理检测:用当前代理访问测试 URL验证是否可用
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")):
"""检测代理是否可用:用解析到的代理请求测试页,返回是否成功及来源。"""
proxy_url = (proxy or "").strip()
source = "query"
if not proxy_url:
proxy_url = _resolve_proxy("", allow_auto=True)
if proxy_url == "__tunnel__":
proxy_url = _proxy_from_tunnel()
source = "tunnel"
logger.info("check-proxy: using tunnel -> %s", "socks5h://***@%s/" % TUNNEL_PROXY)
elif proxy_url == "__kdl__":
proxy_url = await _proxy_from_kdl()
source = "kdl"
logger.info("check-proxy: using kdl -> %s", "http://***@%s/" % (proxy_url.split("@", 1)[-1].rstrip("/") if proxy_url else "?"))
else:
source = "env" if proxy_url else "none"
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。",
}
# 脱敏显示(不暴露密码)
def _preview(u: str) -> str:
if not u or "@" not in u:
return u[:50] + "" if len(u) > 50 else u
pre, at = u.rsplit("@", 1)
if "://" in pre:
scheme = pre.split("://", 1)[0] + "://"
rest = pre[len(scheme) :]
if ":" in rest:
user, _ = rest.split(":", 1)
pre = scheme + user + ":***"
else:
pre = scheme + "***"
else:
pre = "***"
return pre + "@" + (at[:30] + "" if len(at) > 30 else at)
preview = _preview(proxy_url)
logger.info("check-proxy: source=%s, proxy_preview=%s", source, preview)
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:
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,
}
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,
}
except Exception as e:
logger.warning("check-proxy: exception %s", e)
return {
"ok": False,
"source": source,
"proxy_preview": preview,
"error": str(e),
}
def _proxy_from_env() -> str:
"""当登录页未填代理时,使用环境变量中的代理(服务器上设置 HTTP_PROXY/HTTPS_PROXY 后生效)。"""
return (
@@ -390,26 +468,170 @@ def _proxy_from_env() -> str:
)
# 固定隧道代理socks5h + 用户名密码):未配置登录页/环境变量时使用
# 格式与 requests 示例一致socks5h://user:pwd@host:port/
TUNNEL_PROXY = (os.getenv("TUNNEL_PROXY") or "").strip() # 例如 218.78.109.253:16816
TUNNEL_PROXY_USERNAME = (os.getenv("TUNNEL_PROXY_USERNAME") or "").strip()
TUNNEL_PROXY_PASSWORD = (os.getenv("TUNNEL_PROXY_PASSWORD") or "").strip()
# 快代理 KDL API可选隧道未配置时从此接口拉取代理
KDL_PROXY_API_URL = (os.getenv("KDL_PROXY_API_URL") or "").strip()
KDL_PROXY_USERNAME = (os.getenv("KDL_PROXY_USERNAME") or "").strip()
KDL_PROXY_PASSWORD = (os.getenv("KDL_PROXY_PASSWORD") or "").strip()
# 启动时打印代理配置情况(不打印密码)
def _log_proxy_config() -> None:
tunnel_ok = bool(TUNNEL_PROXY and TUNNEL_PROXY_USERNAME and TUNNEL_PROXY_PASSWORD)
kdl_ok = bool(KDL_PROXY_API_URL and KDL_PROXY_USERNAME and KDL_PROXY_PASSWORD)
logger.info(
"proxy config: tunnel=%s (TUNNEL_PROXY=%s), kdl=%s (KDL_API=%s)",
tunnel_ok,
TUNNEL_PROXY or "(empty)",
kdl_ok,
"set" if KDL_PROXY_API_URL else "(empty)",
)
_log_proxy_config()
def _proxy_from_tunnel() -> str:
"""使用固定隧道代理,格式 socks5h://user:pwd@host:port/,供 7006 使用。"""
if not TUNNEL_PROXY:
return ""
user = (TUNNEL_PROXY_USERNAME or KDL_PROXY_USERNAME or "").strip()
pwd = (TUNNEL_PROXY_PASSWORD or KDL_PROXY_PASSWORD or "").strip()
if not user or not pwd:
return ""
return "socks5h://%(user)s:%(pwd)s@%(proxy)s/" % {
"user": user,
"pwd": pwd,
"proxy": TUNNEL_PROXY,
}
async def _proxy_from_kdl() -> str:
"""从快代理 API 获取一个代理 IP格式化为 http://user:pwd@ip:port/ 供 7006 使用。"""
if not KDL_PROXY_API_URL or not KDL_PROXY_USERNAME or not KDL_PROXY_PASSWORD:
return ""
try:
async with httpx.AsyncClient(trust_env=False, timeout=10.0) as client:
resp = await client.get(KDL_PROXY_API_URL)
resp.raise_for_status()
proxy_ip = (resp.text or "").strip()
if not proxy_ip:
return ""
return "http://%(user)s:%(pwd)s@%(proxy)s/" % {
"user": KDL_PROXY_USERNAME,
"pwd": KDL_PROXY_PASSWORD,
"proxy": proxy_ip,
}
except Exception as e:
logger.warning("KDL proxy fetch failed: %s", e)
return ""
# 隧道代理若以 http 形式传入(登录页或 KDL 返回),统一改为 socks5h 再传给 7006
TUNNEL_PROXY_NORMALIZE_HOST = (os.getenv("TUNNEL_PROXY_NORMALIZE_HOST") or "218.78.109.253:16816").strip()
def _proxy_preview_for_log(proxy: str) -> str:
"""代理脱敏,用于日志打印(不暴露密码)。"""
if not proxy or not isinstance(proxy, str):
return "(empty)"
u = proxy.strip()
if not u:
return "(empty)"
if "@" not in u:
return u[:50] + "" if len(u) > 50 else u
pre, at = u.rsplit("@", 1)
at = at.rstrip("/").split("/")[0].split("?")[0]
if "://" in pre:
scheme = pre.split("://", 1)[0] + "://"
rest = pre[len(scheme):]
user = rest.split(":", 1)[0] if ":" in rest else "***"
pre = scheme + user + ":***"
else:
pre = "***"
return pre + "@" + (at[:40] + "" if len(at) > 40 else at) + "/"
def _normalize_proxy_scheme_to_socks5h(proxy: str) -> str:
"""若代理是隧道地址但用了 http改为 socks5h7006 需 socks5"""
if not proxy or not isinstance(proxy, str):
return proxy
p = proxy.strip()
if not p.startswith("http://") or "@" not in p:
return p
try:
host_part = p.split("@", 1)[1].rstrip("/").split("/")[0].split("?")[0]
except IndexError:
return p
if host_part != TUNNEL_PROXY and host_part != "218.78.109.253:16816":
return p
out = "socks5h://" + p[7:]
logger.info("proxy normalize: http -> socks5h for tunnel %s", host_part)
return out
def _resolve_proxy(body_proxy: str, *, allow_auto: bool = True) -> str:
"""解析最终传给 7006 的代理:请求体 > 环境变量 > 固定隧道 >可选KDL API。"""
p = (body_proxy or "").strip()
if p:
logger.debug("proxy resolve: from body, len=%s", len(p))
return p
p = _proxy_from_env()
if p:
logger.debug("proxy resolve: from env (HTTP_PROXY/HTTPS_PROXY), len=%s", len(p))
return p
if not allow_auto:
return ""
# 隧道:只要 TUNNEL_PROXY 有值且能凑齐账号密码(含用 KDL_* 兜底)则优先隧道
tunnel_user = (TUNNEL_PROXY_USERNAME or KDL_PROXY_USERNAME or "").strip()
tunnel_pwd = (TUNNEL_PROXY_PASSWORD or KDL_PROXY_PASSWORD or "").strip()
if TUNNEL_PROXY and tunnel_user and tunnel_pwd:
logger.info("proxy resolve: auto -> tunnel (socks5h), TUNNEL_PROXY=%s", TUNNEL_PROXY)
return "__tunnel__"
if KDL_PROXY_API_URL and KDL_PROXY_USERNAME and KDL_PROXY_PASSWORD:
logger.info("proxy resolve: auto -> kdl (fetch from API)")
return "__kdl__"
logger.debug("proxy resolve: no auto proxy configured")
return ""
@app.post("/auth/wake")
async def wake_up_login(body: WakeUpRequest):
"""唤醒登录:仅调用上游 /login/WakeUpLogin只限扫码登录不获取二维码。"""
key = (body.key or "").strip()
if not key:
raise HTTPException(status_code=400, detail="key is required")
proxy = (body.Proxy or "").strip()
if not proxy:
proxy = _proxy_from_env()
proxy = _resolve_proxy(body.Proxy or "", allow_auto=True)
if proxy == "__tunnel__":
proxy = _proxy_from_tunnel()
if proxy:
logger.info("WakeUpLogin: using proxy from env (HTTP_PROXY/HTTPS_PROXY), len=%s", len(proxy))
else:
logger.info("WakeUpLogin: Proxy 为空,请在 .env 中设置 HTTP_PROXY/HTTPS_PROXY或登录页填写代理后重试")
logger.info("WakeUpLogin: using proxy from tunnel (socks5h), len=%s", len(proxy))
elif proxy == "__kdl__":
proxy = await _proxy_from_kdl()
if proxy:
logger.info("WakeUpLogin: using proxy from KDL API, len=%s", len(proxy))
if not proxy:
logger.info("WakeUpLogin: Proxy 为空,请在 .env 中配置 TUNNEL_PROXY 或 HTTP_PROXY/HTTPS_PROXY 或 KDL或登录页填写代理")
elif proxy not in ("__tunnel__", "__kdl__"):
logger.info("WakeUpLogin: using proxy from body/env, len=%s", len(proxy))
if proxy in ("__tunnel__", "__kdl__"):
proxy = ""
proxy = _normalize_proxy_scheme_to_socks5h(proxy)
ipad_ormac = (body.IpadOrmac or "").strip() or "ipad"
payload = {
"Check": body.Check,
"IpadOrmac": "ipad",
"IpadOrmac": ipad_ormac,
"Proxy": proxy,
}
url = f"{WECHAT_UPSTREAM_BASE_URL.rstrip('/')}/login/WakeUpLogin"
logger.info("WakeUpLogin: key=%s, payload=%s, url=%s", key, payload, url)
logger.info(
"WakeUpLogin 请求参数: key=%s, url=%s, Proxy=%s, Check=%s, IpadOrmac=%s",
key, url, proxy, body.Check, ipad_ormac,
)
try:
async with httpx.AsyncClient(trust_env=False, timeout=20.0) as client:
resp = await client.post(url, params={"key": key}, json=payload)
@@ -440,22 +662,32 @@ async def get_login_qrcode(body: QrCodeRequest):
if not key:
raise HTTPException(status_code=400, detail="key is required")
proxy = (body.Proxy or "").strip()
if not proxy:
proxy = _proxy_from_env()
if not proxy:
raise HTTPException(
status_code=400,
detail="必须配置代理Proxy。服务器在香港不上代理必封号请填写 socks5 代理后再取码。",
)
proxy = _resolve_proxy(body.Proxy or "", allow_auto=True)
if proxy == "__tunnel__":
proxy = _proxy_from_tunnel()
if proxy:
logger.info("GetLoginQrCodeNewDirect: using proxy from tunnel (socks5h), len=%s", len(proxy))
elif proxy == "__kdl__":
proxy = await _proxy_from_kdl()
if proxy:
logger.info("GetLoginQrCodeNewDirect: using proxy from KDL API, len=%s", len(proxy))
if proxy in ("__tunnel__", "__kdl__"):
proxy = ""
proxy = _normalize_proxy_scheme_to_socks5h(proxy)
if proxy:
logger.info("GetLoginQrCodeNewDirect: proxy=yes, force_mac=%s, IpadOrmac=%s", body.force_mac, "mac" if body.force_mac else (body.IpadOrmac or "ipad"))
else:
logger.info("GetLoginQrCodeNewDirect: proxy=empty未配置则后端自动读 env/KDLforce_mac=%s", body.force_mac)
payload = body.dict(exclude={"key", "force_mac"})
payload["Check"] = False
payload["IpadOrmac"] = "mac" if body.force_mac else "ipad"
payload["IpadOrmac"] = "mac" if body.force_mac else ((body.IpadOrmac or "").strip() or "ipad")
payload["Proxy"] = proxy
logger.info("GetLoginQrCodeNewDirect: proxy=yes, force_mac=%s, IpadOrmac=%s", body.force_mac, payload["IpadOrmac"])
url = f"{WECHAT_UPSTREAM_BASE_URL}/login/GetLoginQrCodeNewDirect"
logger.info("GetLoginQrCodeNewDirect: key=%s, payload=%s, url=%s", key, payload, url)
logger.info(
"GetLoginQrCodeNewDirect 请求参数: key=%s, url=%s, Proxy=%s, Check=%s, IpadOrmac=%s",
key, url, proxy, False, payload["IpadOrmac"],
)
try:
async with httpx.AsyncClient(trust_env=False, timeout=20.0) as client:
resp = await client.post(url, params={"key": key}, json=payload)