#!/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 '关'}" )