from __future__ import annotations import copy import json import os import shutil import tempfile import time import urllib.error import urllib.request from datetime import datetime from pathlib import Path from typing import Any, Dict, Iterable, List, Optional from openai_pool_orchestrator import CONFIG_FILE, STATE_FILE, TOKENS_DIR from openai_pool_orchestrator.pool_maintainer import PoolMaintainer, Sub2ApiMaintainer SUB2API_MAINTAIN_ACTION_DEFAULTS: Dict[str, bool] = { "refresh_abnormal_accounts": True, "delete_abnormal_accounts": True, "dedupe_duplicate_accounts": True, } UPLOAD_PLATFORMS = ("cpa", "sub2api") DEFAULT_CONFIG: Dict[str, Any] = { "base_url": "", "bearer_token": "", "account_name": "AutoReg", "auto_sync": False, "cpa_base_url": "", "cpa_token": "", "min_candidates": 800, "used_percent_threshold": 95, "auto_maintain": False, "maintain_interval_minutes": 30, "upload_mode": "snapshot", "mail_provider": "mailtm", "mail_config": {"api_base": "https://api.mail.tm", "api_key": "", "bearer_token": ""}, "sub2api_min_candidates": 200, "sub2api_auto_maintain": False, "sub2api_maintain_interval_minutes": 30, "sub2api_maintain_actions": copy.deepcopy(SUB2API_MAINTAIN_ACTION_DEFAULTS), "proxy": "", "auto_register": False, "auto_register_max_per_loop": 1, "proxy_pool_enabled": True, "proxy_pool_api_url": "https://zenproxy.top/api/fetch", "proxy_pool_auth_mode": "query", "proxy_pool_api_key": "19c0ec43-8f76-4c97-81bc-bcda059eeba4", "proxy_pool_count": 1, "proxy_pool_country": "US", } def _as_bool(value: Any, default: bool = False) -> bool: if isinstance(value, bool): return value if value is None: return default if isinstance(value, (int, float)): return bool(value) text = str(value).strip().lower() if text in ("1", "true", "yes", "on"): return True if text in ("0", "false", "no", "off", ""): return False return default def _normalize_service_url(value: Any) -> str: text = str(value or "").strip() if not text: return "" if not text.startswith(("http://", "https://")): return "" return text.rstrip("/") def normalize_sub2api_maintain_actions(raw: Any) -> Dict[str, bool]: source = raw if isinstance(raw, dict) else {} return { key: _as_bool(source.get(key, default), default=default) for key, default in SUB2API_MAINTAIN_ACTION_DEFAULTS.items() } def _write_json_atomic(path: Path, payload: Dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) fd, tmp_path = tempfile.mkstemp(prefix=f".{path.stem}_", suffix=path.suffix, dir=str(path.parent)) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(payload, handle, ensure_ascii=False, indent=2) handle.flush() os.fsync(handle.fileno()) os.replace(tmp_path, path) finally: try: if os.path.exists(tmp_path): os.remove(tmp_path) except OSError: pass def load_sync_config() -> Dict[str, Any]: if CONFIG_FILE.exists(): try: return normalize_config(json.loads(CONFIG_FILE.read_text(encoding="utf-8"))) except Exception: pass return normalize_config(copy.deepcopy(DEFAULT_CONFIG)) def has_initialized_config() -> bool: if not CONFIG_FILE.exists(): return False try: data = json.loads(CONFIG_FILE.read_text(encoding="utf-8")) except Exception: return False if not isinstance(data, dict): return False proxy = str(data.get("proxy") or "").strip() providers = data.get("mail_providers") or [] cpa_base_url = str(data.get("cpa_base_url") or "").strip() base_url = str(data.get("base_url") or "").strip() bearer_token = str(data.get("bearer_token") or "").strip() email = str(data.get("email") or "").strip() password = str(data.get("password") or "").strip() provider_ready = isinstance(providers, list) and any(str(item).strip() for item in providers) sub2api_ready = bool(base_url and (bearer_token or (email and password))) cpa_ready = bool(cpa_base_url and str(data.get("cpa_token") or "").strip()) return bool(proxy and provider_ready) or sub2api_ready or cpa_ready def normalize_config(cfg: Dict[str, Any]) -> Dict[str, Any]: cfg = copy.deepcopy(cfg or {}) legacy = str(cfg.get("mail_provider", "mailtm") or "mailtm").strip().lower() legacy_cfg = cfg.get("mail_config") or {} if not isinstance(legacy_cfg, dict): legacy_cfg = {} raw_providers = cfg.get("mail_providers") providers = raw_providers if isinstance(raw_providers, list) else [] providers = [str(name).strip().lower() for name in providers if str(name).strip()] if not providers: providers = [legacy] raw_cfgs = cfg.get("mail_provider_configs") provider_cfgs = raw_cfgs if isinstance(raw_cfgs, dict) else {} for name in providers: if name not in provider_cfgs or not isinstance(provider_cfgs.get(name), dict): provider_cfgs[name] = {} if legacy in provider_cfgs: for key, value in legacy_cfg.items(): provider_cfgs[legacy].setdefault(key, value) strategy = str(cfg.get("mail_strategy", "round_robin") or "round_robin").strip().lower() if strategy not in ("round_robin", "random", "failover"): strategy = "round_robin" upload_mode = str(cfg.get("upload_mode", "snapshot") or "snapshot").strip().lower() if upload_mode not in ("snapshot", "decoupled"): upload_mode = "snapshot" cfg["mail_providers"] = providers cfg["mail_provider_configs"] = provider_cfgs cfg["mail_strategy"] = strategy cfg["mail_provider"] = providers[0] cfg["upload_mode"] = upload_mode cfg["auto_sync"] = _as_bool(cfg.get("auto_sync", False), default=False) cfg["auto_maintain"] = _as_bool(cfg.get("auto_maintain", False), default=False) cfg["sub2api_auto_maintain"] = _as_bool(cfg.get("sub2api_auto_maintain", False), default=False) cfg["sub2api_maintain_actions"] = normalize_sub2api_maintain_actions(cfg.get("sub2api_maintain_actions")) cfg["auto_register"] = _as_bool(cfg.get("auto_register", False), default=False) try: cfg["auto_register_max_per_loop"] = max(1, min(int(cfg.get("auto_register_max_per_loop", 1)), 20)) except (TypeError, ValueError): cfg["auto_register_max_per_loop"] = 1 cfg["proxy_pool_enabled"] = _as_bool(cfg.get("proxy_pool_enabled", True), default=True) proxy_pool_api_url = str(cfg.get("proxy_pool_api_url", DEFAULT_CONFIG["proxy_pool_api_url"]) or "").strip() cfg["proxy_pool_api_url"] = proxy_pool_api_url or DEFAULT_CONFIG["proxy_pool_api_url"] proxy_pool_auth_mode = str(cfg.get("proxy_pool_auth_mode", "query") or "").strip().lower() if proxy_pool_auth_mode not in ("header", "query"): proxy_pool_auth_mode = "query" cfg["proxy_pool_auth_mode"] = proxy_pool_auth_mode cfg["proxy_pool_api_key"] = str(cfg.get("proxy_pool_api_key", DEFAULT_CONFIG["proxy_pool_api_key"]) or "").strip() try: cfg["proxy_pool_count"] = max(1, min(int(cfg.get("proxy_pool_count", 1)), 20)) except (TypeError, ValueError): cfg["proxy_pool_count"] = 1 cfg["proxy_pool_country"] = str(cfg.get("proxy_pool_country", "US") or "US").strip().upper() or "US" cpa_base_url = str(cfg.get("cpa_base_url", "") or "").strip().rstrip("/") if cpa_base_url.lower().endswith("/v0"): cpa_base_url = cpa_base_url[:-3].rstrip("/") cfg["cpa_base_url"] = cpa_base_url return {**copy.deepcopy(DEFAULT_CONFIG), **cfg} def save_sync_config(cfg: Dict[str, Any]) -> Dict[str, Any]: normalized = normalize_config(cfg) _write_json_atomic(CONFIG_FILE, normalized) return normalized def init_config_from_example(project_root: Path) -> Path: example_path = project_root / "config" / "sync_config.example.json" CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) if CONFIG_FILE.exists(): return CONFIG_FILE if example_path.exists(): shutil.copyfile(example_path, CONFIG_FILE) else: _write_json_atomic(CONFIG_FILE, copy.deepcopy(DEFAULT_CONFIG)) return CONFIG_FILE def load_state() -> Dict[str, int]: if STATE_FILE.exists(): try: data = json.loads(STATE_FILE.read_text(encoding="utf-8")) if isinstance(data, dict): return { "success": int(data.get("success", 0) or 0), "fail": int(data.get("fail", 0) or 0), } except Exception: pass return {"success": 0, "fail": 0} def iter_token_files() -> Iterable[Path]: if not TOKENS_DIR.exists(): return [] return sorted(TOKENS_DIR.glob("*.json"), key=lambda path: path.name, reverse=True) def read_token_file(path: Path) -> Dict[str, Any]: data = json.loads(path.read_text(encoding="utf-8")) if not isinstance(data, dict): raise ValueError(f"token file is not a JSON object: {path.name}") return data def extract_uploaded_platforms(token_data: Dict[str, Any]) -> List[str]: platforms = set() raw_platforms = token_data.get("uploaded_platforms") if isinstance(raw_platforms, list): for item in raw_platforms: name = str(item).strip().lower() if name in UPLOAD_PLATFORMS: platforms.add(name) if token_data.get("cpa_uploaded") or token_data.get("cpa_synced"): platforms.add("cpa") if token_data.get("sub2api_uploaded") or token_data.get("sub2api_synced") or token_data.get("synced"): platforms.add("sub2api") return [name for name in UPLOAD_PLATFORMS if name in platforms] def is_sub2api_uploaded(token_data: Dict[str, Any]) -> bool: return "sub2api" in extract_uploaded_platforms(token_data) def mark_token_uploaded_platform(file_path: Path, platform: str) -> bool: platform_name = str(platform).strip().lower() if platform_name not in UPLOAD_PLATFORMS: return False try: token_data = read_token_file(file_path) platforms = extract_uploaded_platforms(token_data) if platform_name not in platforms: platforms.append(platform_name) token_data["uploaded_platforms"] = [name for name in UPLOAD_PLATFORMS if name in set(platforms)] token_data[f"{platform_name}_uploaded"] = True token_data[f"{platform_name}_synced"] = True if platform_name == "sub2api": token_data["synced"] = True uploaded_at = token_data.get("uploaded_at") if not isinstance(uploaded_at, dict): uploaded_at = {} uploaded_at[platform_name] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") token_data["uploaded_at"] = uploaded_at _write_json_atomic(file_path, token_data) return True except Exception: return False def get_pool_maintainer(cfg: Optional[Dict[str, Any]] = None) -> Optional[PoolMaintainer]: config = cfg or load_sync_config() base_url = _normalize_service_url(config.get("cpa_base_url", "")) token = str(config.get("cpa_token", "")).strip() if not base_url or not token: return None return PoolMaintainer( cpa_base_url=base_url, cpa_token=token, min_candidates=int(config.get("min_candidates", 800)), used_percent_threshold=int(config.get("used_percent_threshold", 95)), ) def get_sub2api_maintainer(cfg: Optional[Dict[str, Any]] = None) -> Optional[Sub2ApiMaintainer]: config = cfg or load_sync_config() base_url = _normalize_service_url(config.get("base_url", "")) bearer = str(config.get("bearer_token", "")).strip() email = str(config.get("email", "")).strip() password = str(config.get("password", "")).strip() if not base_url: return None if not bearer and not (email and password): return None return Sub2ApiMaintainer( base_url=base_url, bearer_token=bearer, min_candidates=int(config.get("sub2api_min_candidates", 200)), email=email, password=password, ) def verify_sub2api_login(base_url: str, email: str, password: str) -> Dict[str, Any]: from curl_cffi import requests as cffi_req url = base_url.strip() if not url.startswith(("http://", "https://")): url = "https://" + url login_url = url.rstrip("/") + "/api/v1/auth/login" try: resp = cffi_req.post( login_url, json={"email": email, "password": password}, impersonate="chrome", timeout=15, ) raw_body = resp.text if resp.status_code != 200: try: err_body = json.loads(raw_body) err_msg = err_body.get("message") or err_body.get("error") or raw_body[:200] except json.JSONDecodeError: err_msg = raw_body[:200] return {"ok": False, "error": f"登录失败(HTTP {resp.status_code}): {err_msg}"} body = json.loads(raw_body) token = ( body.get("token") or body.get("access_token") or (body.get("data") or {}).get("token") or (body.get("data") or {}).get("access_token") or "" ) return {"ok": True, "token": token} except Exception as exc: return {"ok": False, "error": f"请求异常: {exc}"} def verify_sub2api_token(base_url: str, bearer_token: str) -> Dict[str, Any]: from curl_cffi import requests as cffi_req url = base_url.strip() if not url.startswith(("http://", "https://")): url = "https://" + url verify_url = url.rstrip("/") + "/api/v1/admin/dashboard/stats" try: resp = cffi_req.get( verify_url, headers={"Authorization": f"Bearer {bearer_token}", "Accept": "application/json"}, params={"timezone": "Asia/Shanghai"}, impersonate="chrome", timeout=15, ) if resp.status_code != 200: return {"ok": False, "error": f"Bearer Token 验证失败: HTTP {resp.status_code}"} return {"ok": True} except Exception as exc: return {"ok": False, "error": f"Bearer Token 验证异常: {exc}"} def save_sub2api_credentials( *, base_url: str, bearer_token: str = "", email: str = "", password: str = "", account_name: Optional[str] = None, auto_sync: Optional[bool] = None, ) -> Dict[str, Any]: cfg = load_sync_config() normalized_base_url = base_url.strip() if normalized_base_url and not normalized_base_url.startswith(("http://", "https://")): normalized_base_url = "https://" + normalized_base_url if not normalized_base_url: raise ValueError("请填写平台地址") saved_email = email.strip() or str(cfg.get("email", "") or "").strip() saved_password = password.strip() if password else str(cfg.get("password", "") or "").strip() saved_bearer = bearer_token.strip() or str(cfg.get("bearer_token", "") or "").strip() verified_token = saved_bearer if saved_email and saved_password: verify = verify_sub2api_login(normalized_base_url, saved_email, saved_password) if not verify.get("ok"): raise ValueError(str(verify.get("error") or "登录校验失败")) verified_token = str(verify.get("token") or "").strip() or saved_bearer elif saved_bearer: verify = verify_sub2api_token(normalized_base_url, saved_bearer) if not verify.get("ok"): raise ValueError(str(verify.get("error") or "Token 校验失败")) else: raise ValueError("请填写 Bearer Token 或邮箱和密码") cfg["base_url"] = normalized_base_url cfg["bearer_token"] = verified_token cfg["email"] = saved_email cfg["password"] = saved_password if account_name is not None: cfg["account_name"] = account_name.strip() if auto_sync is not None: cfg["auto_sync"] = bool(auto_sync) return save_sync_config(cfg) def build_account_payload(email: str, token_data: Dict[str, Any]) -> Dict[str, Any]: access_token = token_data.get("access_token", "") refresh_token = token_data.get("refresh_token", "") id_token = token_data.get("id_token", "") at_payload = decode_jwt_payload(access_token) if access_token else {} at_auth = at_payload.get("https://api.openai.com/auth") or {} chatgpt_account_id = at_auth.get("chatgpt_account_id", "") or token_data.get("account_id", "") chatgpt_user_id = at_auth.get("chatgpt_user_id", "") exp_timestamp = at_payload.get("exp", 0) expires_at = exp_timestamp if isinstance(exp_timestamp, int) and exp_timestamp > 0 else int(time.time()) + 863999 it_payload = decode_jwt_payload(id_token) if id_token else {} it_auth = it_payload.get("https://api.openai.com/auth") or {} organization_id = it_auth.get("organization_id", "") if not organization_id: orgs = it_auth.get("organizations") or [] if orgs: organization_id = (orgs[0] or {}).get("id", "") return { "name": email, "notes": "", "platform": "openai", "type": "oauth", "credentials": { "access_token": access_token, "refresh_token": refresh_token, "expires_in": 863999, "expires_at": expires_at, "chatgpt_account_id": chatgpt_account_id, "chatgpt_user_id": chatgpt_user_id, "organization_id": organization_id, }, "extra": {"email": email}, "proxy_id": None, "concurrency": 10, "priority": 1, "rate_multiplier": 1, "group_ids": [2, 4], "expires_at": None, "auto_pause_on_expired": True, } def decode_jwt_payload(token: str) -> Dict[str, Any]: try: import base64 parts = token.split(".") if len(parts) != 3: return {} payload = parts[1] pad = 4 - len(payload) % 4 if pad != 4: payload += "=" * pad decoded = base64.urlsafe_b64decode(payload.encode("ascii")) return json.loads(decoded.decode("utf-8")) except Exception: return {} def push_account_api(base_url: str, bearer: str, email: str, token_data: Dict[str, Any]) -> Dict[str, Any]: from curl_cffi import requests as cffi_req url = base_url.rstrip("/") + "/api/v1/admin/accounts" payload = build_account_payload(email, token_data) try: resp = cffi_req.post( url, json=payload, headers={ "Authorization": f"Bearer {bearer}", "Content-Type": "application/json", "Accept": "application/json, text/plain, */*", "Referer": base_url.rstrip("/") + "/admin/accounts", }, impersonate="chrome", timeout=20, ) return {"ok": resp.status_code in (200, 201), "status": resp.status_code, "body": resp.text[:300]} except Exception as exc: return {"ok": False, "status": 0, "body": str(exc)} def update_sub2api_account_api( base_url: str, bearer: str, account_id: int, email: str, token_data: Dict[str, Any], ) -> Dict[str, Any]: from curl_cffi import requests as cffi_req url = base_url.rstrip("/") + f"/api/v1/admin/accounts/{int(account_id)}" create_payload = build_account_payload(email, token_data) payload = { "name": str(email or "").strip(), "credentials": create_payload.get("credentials") if isinstance(create_payload.get("credentials"), dict) else {}, "extra": create_payload.get("extra") if isinstance(create_payload.get("extra"), dict) else {}, "concurrency": create_payload.get("concurrency", 10), "priority": create_payload.get("priority", 1), "status": "active", "auto_pause_on_expired": True, } try: resp = cffi_req.put( url, json=payload, headers={ "Authorization": f"Bearer {bearer}", "Content-Type": "application/json", "Accept": "application/json, text/plain, */*", "Referer": base_url.rstrip("/") + "/admin/accounts", }, impersonate="chrome", timeout=20, ) return {"ok": resp.status_code in (200, 201), "status": resp.status_code, "body": resp.text[:300]} except Exception as exc: return {"ok": False, "status": 0, "body": str(exc)} def _extract_sub2api_page_payload(body: Any) -> Dict[str, Any]: if isinstance(body, dict): data = body.get("data") if isinstance(data, dict): return data return body return {} def _sub2api_item_matches_identity(item: Dict[str, Any], email: str, refresh_token: str) -> bool: email_norm = str(email or "").strip().lower() refresh_token_norm = str(refresh_token or "").strip() name = str(item.get("name") or "").strip().lower() extra = item.get("extra") if isinstance(item.get("extra"), dict) else {} credentials = item.get("credentials") if isinstance(item.get("credentials"), dict) else {} item_email = str(extra.get("email") or "").strip().lower() item_refresh_token = str(credentials.get("refresh_token") or "").strip() if refresh_token_norm and item_refresh_token and item_refresh_token == refresh_token_norm: return True if email_norm and (name == email_norm or item_email == email_norm): return True return False def find_existing_sub2api_account( base_url: str, bearer: str, email: str, refresh_token: str, max_pages: int = 8, ) -> Optional[Dict[str, Any]]: from curl_cffi import requests as cffi_req url = base_url.rstrip("/") + "/api/v1/admin/accounts" email_norm = str(email or "").strip().lower() refresh_token_norm = str(refresh_token or "").strip() if not email_norm and not refresh_token_norm: return None headers = {"Authorization": f"Bearer {bearer}", "Accept": "application/json, text/plain, */*"} page_size = 100 page = 1 scanned_without_search = 0 while page <= max_pages: params: Dict[str, Any] = {"page": page, "page_size": page_size, "platform": "openai", "type": "oauth"} if email_norm: params["search"] = email_norm try: resp = cffi_req.get(url, params=params, headers=headers, impersonate="chrome", timeout=15) if resp.status_code != 200: return None body = resp.json() except Exception: return None data = _extract_sub2api_page_payload(body) items = data.get("items") if isinstance(data.get("items"), list) else [] for item in items: if isinstance(item, dict) and _sub2api_item_matches_identity(item, email_norm, refresh_token_norm): return item total_raw = data.get("total") try: total = int(total_raw) if total_raw is not None else 0 except (TypeError, ValueError): total = 0 if len(items) < page_size or (total > 0 and page * page_size >= total): break page += 1 if refresh_token_norm: page = 1 while page <= 3: params = {"page": page, "page_size": page_size, "platform": "openai", "type": "oauth"} try: resp = cffi_req.get(url, params=params, headers=headers, impersonate="chrome", timeout=15) if resp.status_code != 200: return None body = resp.json() except Exception: return None data = _extract_sub2api_page_payload(body) items = data.get("items") if isinstance(data.get("items"), list) else [] for item in items: if isinstance(item, dict) and _sub2api_item_matches_identity(item, "", refresh_token_norm): return item scanned_without_search += len(items) if len(items) < page_size or scanned_without_search >= 300: break page += 1 return None def push_account_api_with_dedupe( base_url: str, bearer: str, email: str, token_data: Dict[str, Any], check_before: bool = True, check_after: bool = True, ) -> Dict[str, Any]: refresh_token = str(token_data.get("refresh_token") or "").strip() existing: Optional[Dict[str, Any]] = None if check_before: existing = find_existing_sub2api_account(base_url, bearer, email, refresh_token) if existing is not None: existing_id = existing.get("id") try: existing_int = int(existing_id) except (TypeError, ValueError): existing_int = None if existing_int is not None and existing_int > 0: update_result = update_sub2api_account_api(base_url, bearer, existing_int, email, token_data) if update_result.get("ok"): return { "ok": True, "status": int(update_result.get("status") or 200), "body": "existing account updated", "skipped": False, "reason": "updated_existing_before_create", "existing_id": existing_int, } return { "ok": False, "status": int(update_result.get("status") or 0), "body": "existing account update failed", "skipped": False, "reason": "exists_before_create_update_failed", "existing_id": existing_int, } return { "ok": True, "status": 200, "body": "account already exists", "skipped": True, "reason": "exists_before_create", "existing_id": existing_id, } result = push_account_api(base_url, bearer, email, token_data) if result.get("ok"): result["skipped"] = False return result if check_after: existing = find_existing_sub2api_account(base_url, bearer, email, refresh_token) if existing is not None: return { "ok": True, "status": int(result.get("status") or 200), "body": "request failed but account exists", "skipped": True, "reason": "exists_after_create", "existing_id": existing.get("id"), } result.setdefault("skipped", False) return result def sync_token_to_sub2api(file_path: Path, cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: config = cfg or load_sync_config() base_url = str(config.get("base_url", "") or "").strip() bearer = str(config.get("bearer_token", "") or "").strip() if not base_url or not bearer: raise ValueError("请先配置 Sub2Api 平台地址和 Bearer Token") token_data = read_token_file(file_path) email = str(token_data.get("email") or file_path.name) result = push_account_api_with_dedupe(base_url, bearer, email, token_data, check_before=True, check_after=True) if result.get("ok"): mark_token_uploaded_platform(file_path, "sub2api") return {"file": file_path.name, "email": email, **result} def sync_all_tokens_to_sub2api(cfg: Optional[Dict[str, Any]] = None, skip_uploaded: bool = True) -> Dict[str, Any]: config = cfg or load_sync_config() results = [] for path in iter_token_files(): try: token_data = read_token_file(path) if skip_uploaded and is_sub2api_uploaded(token_data): results.append({"file": path.name, "email": token_data.get("email", path.name), "ok": True, "skipped": True}) continue results.append(sync_token_to_sub2api(path, config)) except Exception as exc: results.append({"file": path.name, "ok": False, "error": str(exc)}) return summarize_results(results) def upload_all_tokens_to_cpa(cfg: Optional[Dict[str, Any]] = None, skip_uploaded: bool = True) -> Dict[str, Any]: config = cfg or load_sync_config() maintainer = get_pool_maintainer(config) if not maintainer: raise ValueError("请先配置 CPA 地址和 Token") results = [] for path in iter_token_files(): try: token_data = read_token_file(path) if skip_uploaded and "cpa" in extract_uploaded_platforms(token_data): results.append({"file": path.name, "email": token_data.get("email", path.name), "ok": True, "skipped": True}) continue ok = maintainer.upload_token(path.name, token_data, proxy="") if ok: mark_token_uploaded_platform(path, "cpa") results.append({"file": path.name, "email": token_data.get("email", path.name), "ok": ok, "skipped": False}) except Exception as exc: results.append({"file": path.name, "ok": False, "error": str(exc)}) return summarize_results(results) def summarize_results(results: List[Dict[str, Any]]) -> Dict[str, Any]: ok_count = sum(1 for item in results if item.get("ok") and not item.get("skipped")) skip_count = sum(1 for item in results if item.get("skipped")) fail_count = sum(1 for item in results if not item.get("ok")) return {"total": len(results), "ok": ok_count, "skipped": skip_count, "fail": fail_count, "results": results} def print_json(data: Any) -> None: print(json.dumps(data, ensure_ascii=False, indent=2)) def print_status_block(title: str, data: Dict[str, Any]) -> None: print(title) for key, value in data.items(): print(f"- {key}: {value}") def sub2api_actions_description(actions: Dict[str, bool]) -> str: labels: List[str] = [] if actions.get("refresh_abnormal_accounts"): labels.append("异常测活") if actions.get("delete_abnormal_accounts"): labels.append("异常清理") if actions.get("dedupe_duplicate_accounts"): labels.append("重复清理") return "、".join(labels) if labels else "无动作" def save_runtime_proxy(proxy: str, auto_register: Optional[bool] = None) -> Dict[str, Any]: cfg = load_sync_config() cfg["proxy"] = proxy.strip() if auto_register is not None: cfg["auto_register"] = bool(auto_register) return save_sync_config(cfg) def check_proxy(proxy: str) -> Dict[str, Any]: from curl_cffi import requests as cffi_req import re proxy_text = proxy.strip() proxies = {"http": proxy_text, "https": proxy_text} if proxy_text else None try: try: resp = cffi_req.get( "https://cloudflare.com/cdn-cgi/trace", proxies=proxies, http_version="v2", impersonate="chrome", timeout=8, ) except Exception as exc: if "HTTP/3 is not supported over an HTTP proxy" not in str(exc): raise resp = cffi_req.get( "https://cloudflare.com/cdn-cgi/trace", proxies=proxies, http_version="v1", impersonate="chrome", timeout=8, ) text = resp.text loc_match = re.search(r"^loc=(.+)$", text, re.MULTILINE) loc = loc_match.group(1) if loc_match else "?" supported = loc not in ("CN", "HK") return {"ok": supported, "loc": loc, "error": None if supported else "所在地不支持"} except Exception as exc: return {"ok": False, "loc": None, "error": str(exc)} def login_sub2api_once(base_url: str, email: str, password: str) -> Dict[str, Any]: url = base_url.strip() if not url: raise ValueError("请填写平台地址") if not url.startswith(("http://", "https://")): url = "https://" + url login_url = url.rstrip("/") + "/api/v1/auth/login" payload = json.dumps({"email": email, "password": password}).encode("utf-8") request = urllib.request.Request( login_url, data=payload, method="POST", headers={"Content-Type": "application/json", "Accept": "application/json"}, ) try: with urllib.request.urlopen(request, timeout=15) as resp: raw_body = resp.read().decode("utf-8") body = json.loads(raw_body) except urllib.error.HTTPError as exc: raw = exc.read().decode("utf-8", "replace") try: err_body = json.loads(raw) err_msg = err_body.get("message") or err_body.get("error") or raw[:200] except json.JSONDecodeError: err_msg = raw[:200] raise ValueError(f"登录失败: {err_msg}") from exc except Exception as exc: raise ValueError(f"请求异常: {exc}") from exc token = ( body.get("token") or body.get("access_token") or (body.get("data") or {}).get("token") or (body.get("data") or {}).get("access_token") or "" ) if not token: raise ValueError(f"响应中未找到 token 字段: {str(body)[:300]}") cfg = load_sync_config() cfg["base_url"] = url cfg["bearer_token"] = token cfg["email"] = email cfg["password"] = password save_sync_config(cfg) return {"ok": True, "token_preview": token[:16] + "..."}