Files
grok-keygen-new/proxy_pool.py
T
chaos a4dbe948b2 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.
2026-07-13 07:16:11 +08:00

340 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 '关'}"
)