Disable proxy pool by default; add managed proxy lifecycle and Turnstile fixes.
Keep proxy_pool_enabled/managed off for direct registration, while shipping SSO OAuth CPA export, visible Turnstile click handling, and proxy harvest tooling.
This commit is contained in:
1 parent
1ed18236b9
commit
a4dbe948b2
118 files changed
+5351
-318
No files matched your search
+339
@@ -0,0 +1,339 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""注册机代理池:解析 / 轮换 / 线程绑定 / 对接 proxy_manager 库存。
|
||||
|
||||
配置(config.json)示例:
|
||||
"proxy_pool_enabled": true,
|
||||
"proxy_managed": true, # True=用 proxy_alive.json(accounts.x.ai 测通过的)
|
||||
"proxy_pool_file": "./proxies.txt",
|
||||
"proxy_pool_mode": "weighted", # weighted | round_robin | random
|
||||
"proxy_rotate_each_account": true,
|
||||
"proxy_mail_direct": true
|
||||
|
||||
线程绑定:
|
||||
set_thread_proxy(url) / get_thread_proxy()
|
||||
浏览器、HTTP(xAI)、CPA mint 共用同一线程代理,避免出口不一致。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
_lock = threading.Lock()
|
||||
_thread = threading.local()
|
||||
_pool: list[str] = []
|
||||
_index = 0
|
||||
_mode = "weighted"
|
||||
_enabled = False
|
||||
_rotate_each = True
|
||||
_mail_direct = True
|
||||
_managed = False
|
||||
_loaded = False
|
||||
_cfg: dict = {}
|
||||
|
||||
|
||||
def proxy_log_label(proxy: str | None) -> str:
|
||||
p = (proxy or "").strip()
|
||||
if not p:
|
||||
return "(direct)"
|
||||
try:
|
||||
u = urlparse(p if "://" in p else f"http://{p}")
|
||||
host = u.hostname or "?"
|
||||
port = f":{u.port}" if u.port else ""
|
||||
auth = "user:***@" if u.username else ""
|
||||
return f"{u.scheme or 'http'}://{auth}{host}{port}"
|
||||
except Exception:
|
||||
return "(proxy)"
|
||||
|
||||
|
||||
def normalize_proxy(raw: str | None) -> str:
|
||||
p = (raw or "").strip()
|
||||
if not p or p.lower() in ("none", "null", "direct", "off", "-"):
|
||||
return ""
|
||||
if "://" not in p:
|
||||
if p.count(":") == 3:
|
||||
host, port, user, pwd = p.split(":", 3)
|
||||
p = f"http://{user}:{pwd}@{host}:{port}"
|
||||
else:
|
||||
p = f"http://{p}"
|
||||
return p
|
||||
|
||||
|
||||
def proxy_for_chromium(proxy: str | None) -> str:
|
||||
"""Chromium --proxy-server:scheme://host:port(不能带 user:pass)。"""
|
||||
p = normalize_proxy(proxy)
|
||||
if not p:
|
||||
return ""
|
||||
u = urlparse(p)
|
||||
host = u.hostname or ""
|
||||
if not host:
|
||||
return ""
|
||||
scheme = (u.scheme or "http").lower()
|
||||
if scheme in ("socks5h",):
|
||||
scheme = "socks5"
|
||||
port = u.port or (443 if scheme == "https" else 80)
|
||||
return f"{scheme}://{host}:{port}"
|
||||
|
||||
|
||||
def proxy_has_auth(proxy: str | None) -> bool:
|
||||
p = normalize_proxy(proxy)
|
||||
if not p:
|
||||
return False
|
||||
u = urlparse(p)
|
||||
return bool(u.username)
|
||||
|
||||
|
||||
def _parse_lines(text: str) -> list[str]:
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for line in (text or "").splitlines():
|
||||
s = line.strip()
|
||||
if not s or s.startswith("#"):
|
||||
continue
|
||||
if "://" not in s and s.count(":") == 3:
|
||||
host, port, user, pwd = s.split(":", 3)
|
||||
s = f"http://{user}:{pwd}@{host}:{port}"
|
||||
p = normalize_proxy(s)
|
||||
if p and p not in seen:
|
||||
seen.add(p)
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def _load_file(path: str) -> list[str]:
|
||||
raw = (path or "").strip()
|
||||
if not raw:
|
||||
return []
|
||||
p = Path(raw).expanduser()
|
||||
if not p.is_absolute():
|
||||
p = (Path(__file__).resolve().parent / p).resolve()
|
||||
if not p.is_file():
|
||||
return []
|
||||
try:
|
||||
return _parse_lines(p.read_text(encoding="utf-8", errors="replace"))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _load_managed_pool(cfg: dict) -> list[str]:
|
||||
"""从 proxy_alive.json 读测通过的库存。"""
|
||||
try:
|
||||
from proxy_store import ProxyStore
|
||||
|
||||
db = cfg.get("proxy_db") or cfg.get("proxy_alive_db") or "./proxy_alive.json"
|
||||
store = ProxyStore(db)
|
||||
return store.proxy_urls(alive_only=True)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def configure_from_config(cfg: dict | None) -> list[str]:
|
||||
"""从 config 装载池;返回当前池列表。"""
|
||||
global _pool, _index, _mode, _enabled, _rotate_each, _mail_direct, _managed, _loaded, _cfg
|
||||
cfg = cfg or {}
|
||||
_cfg = dict(cfg)
|
||||
items: list[str] = []
|
||||
managed = bool(cfg.get("proxy_managed", True))
|
||||
|
||||
if managed and bool(cfg.get("proxy_pool_enabled", False)):
|
||||
items.extend(_load_managed_pool(cfg))
|
||||
|
||||
# 静态列表 / 文件兜底
|
||||
raw_list = cfg.get("proxy_pool") or []
|
||||
if isinstance(raw_list, str):
|
||||
items.extend(_parse_lines(raw_list.replace(",", "\n")))
|
||||
elif isinstance(raw_list, (list, tuple)):
|
||||
for x in raw_list:
|
||||
items.extend(_parse_lines(str(x)))
|
||||
items.extend(_load_file(str(cfg.get("proxy_pool_file") or "")))
|
||||
|
||||
seen: set[str] = set()
|
||||
pool: list[str] = []
|
||||
for p in items:
|
||||
if p not in seen:
|
||||
seen.add(p)
|
||||
pool.append(p)
|
||||
|
||||
enabled = bool(cfg.get("proxy_pool_enabled", bool(pool)))
|
||||
if not pool:
|
||||
single = normalize_proxy(cfg.get("proxy") or "")
|
||||
if single:
|
||||
pool = [single]
|
||||
enabled = bool(cfg.get("proxy_pool_enabled", True)) if single else False
|
||||
managed = False
|
||||
|
||||
mode = str(cfg.get("proxy_pool_mode") or "weighted").strip().lower()
|
||||
if mode not in ("round_robin", "random", "weighted"):
|
||||
mode = "weighted"
|
||||
|
||||
with _lock:
|
||||
_pool = pool
|
||||
_index = 0
|
||||
_mode = mode
|
||||
_enabled = enabled and bool(pool)
|
||||
_rotate_each = bool(cfg.get("proxy_rotate_each_account", True))
|
||||
_mail_direct = bool(cfg.get("proxy_mail_direct", True))
|
||||
_managed = managed and enabled
|
||||
_loaded = True
|
||||
return list(pool)
|
||||
|
||||
|
||||
def reload_from_store() -> int:
|
||||
"""热更新:从 proxy_alive.json 重载(harvest 后调用)。"""
|
||||
global _pool, _enabled
|
||||
if not _cfg:
|
||||
return 0
|
||||
items = _load_managed_pool(_cfg)
|
||||
# 合并静态文件
|
||||
items.extend(_load_file(str(_cfg.get("proxy_pool_file") or "")))
|
||||
seen: set[str] = set()
|
||||
pool = []
|
||||
for p in items:
|
||||
if p not in seen:
|
||||
seen.add(p)
|
||||
pool.append(p)
|
||||
with _lock:
|
||||
_pool = pool
|
||||
_enabled = bool(_cfg.get("proxy_pool_enabled", False)) and bool(pool)
|
||||
return len(pool)
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
return bool(_enabled and _pool)
|
||||
|
||||
|
||||
def is_managed() -> bool:
|
||||
return bool(_managed)
|
||||
|
||||
|
||||
def mail_direct() -> bool:
|
||||
return bool(_mail_direct)
|
||||
|
||||
|
||||
def rotate_each_account() -> bool:
|
||||
return bool(_rotate_each)
|
||||
|
||||
|
||||
def pool_size() -> int:
|
||||
return len(_pool)
|
||||
|
||||
|
||||
def pool_snapshot() -> list[str]:
|
||||
with _lock:
|
||||
return list(_pool)
|
||||
|
||||
|
||||
def set_thread_proxy(proxy: str | None) -> str:
|
||||
p = normalize_proxy(proxy)
|
||||
_thread.proxy = p or None
|
||||
return p
|
||||
|
||||
|
||||
def get_thread_proxy() -> str:
|
||||
return normalize_proxy(getattr(_thread, "proxy", None) or "")
|
||||
|
||||
|
||||
def clear_thread_proxy() -> None:
|
||||
_thread.proxy = None
|
||||
|
||||
|
||||
def acquire(force: bool = False) -> str:
|
||||
"""取下一个代理并绑定当前线程。force=True 强制轮换。"""
|
||||
global _index
|
||||
# managed + weighted:优先走 ProxyStore.pick
|
||||
if _managed and (_mode == "weighted" or force):
|
||||
try:
|
||||
from proxy_manager import get_manager
|
||||
|
||||
mgr = get_manager(_cfg)
|
||||
p = mgr.acquire_for_use()
|
||||
if p:
|
||||
set_thread_proxy(p)
|
||||
# 同步内存池(可能被 drop 过)
|
||||
reload_from_store()
|
||||
return p
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with _lock:
|
||||
if not _enabled or not _pool:
|
||||
p = ""
|
||||
elif _mode == "random":
|
||||
p = random.choice(_pool)
|
||||
else:
|
||||
p = _pool[_index % len(_pool)]
|
||||
_index += 1
|
||||
if not force and not p:
|
||||
cur = get_thread_proxy()
|
||||
if cur:
|
||||
return cur
|
||||
set_thread_proxy(p)
|
||||
return p
|
||||
|
||||
|
||||
def report_success(proxy: str | None = None) -> None:
|
||||
p = normalize_proxy(proxy or get_thread_proxy())
|
||||
if not p or not _managed:
|
||||
return
|
||||
try:
|
||||
from proxy_manager import get_manager
|
||||
|
||||
get_manager(_cfg).report_success(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def report_failure(proxy: str | None = None, reason: str = "") -> None:
|
||||
"""使用失败:从库存删除并热更新内存池。"""
|
||||
p = normalize_proxy(proxy or get_thread_proxy())
|
||||
if not p:
|
||||
return
|
||||
# 内存池先踢
|
||||
with _lock:
|
||||
if p in _pool:
|
||||
_pool[:] = [x for x in _pool if x != p]
|
||||
if not _managed:
|
||||
return
|
||||
try:
|
||||
from proxy_manager import get_manager
|
||||
|
||||
get_manager(_cfg).report_failure(p, reason=reason)
|
||||
reload_from_store()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def resolve_for_http(explicit: str | None = None) -> str:
|
||||
"""HTTP 请求用代理优先级:explicit > 线程绑定 > 池下一枚(仅当启用且线程空) > 空。"""
|
||||
if explicit is not None:
|
||||
return normalize_proxy(explicit)
|
||||
cur = get_thread_proxy()
|
||||
if cur:
|
||||
return cur
|
||||
if is_enabled():
|
||||
return acquire(force=True)
|
||||
return ""
|
||||
|
||||
|
||||
def proxies_dict(proxy: str | None = None) -> dict:
|
||||
p = normalize_proxy(proxy if proxy is not None else resolve_for_http())
|
||||
if not p:
|
||||
return {}
|
||||
return {"http": p, "https": p}
|
||||
|
||||
|
||||
def status_line() -> str:
|
||||
if not is_enabled():
|
||||
cur = get_thread_proxy()
|
||||
if cur:
|
||||
return f"单代理 {proxy_log_label(cur)}"
|
||||
return "直连(未配置代理池)"
|
||||
kind = "managed(x.ai测通)" if _managed else "static"
|
||||
return (
|
||||
f"代理池 {pool_size()} 个 [{kind}] | mode={_mode} | "
|
||||
f"每账号轮换={'开' if _rotate_each else '关'} | "
|
||||
f"邮件直连={'开' if _mail_direct else '关'}"
|
||||
)
|
||||
Reference in new issue
Block a user