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:
chaos committed 2026-07-13 07:16:11 +08:00
1 parent 1ed18236b9
commit a4dbe948b2
118 files changed
+5351 -318

No files matched your search

+339
View File
@@ -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 '关'}"
)