Keep proxy_pool_enabled/managed off for direct registration, while shipping SSO OAuth CPA export, visible Turnstile click handling, and proxy harvest tooling.
340 lines
9.3 KiB
Python
340 lines
9.3 KiB
Python
#!/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 '关'}"
|
||
)
|