Spaces:
Paused
Paused
| """统一远端同步目标分发:CPA / Sub2API。""" | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| from collections.abc import Mapping | |
| from pathlib import Path | |
| from autoteam.textio import parse_env_value | |
| logger = logging.getLogger(__name__) | |
| SYNC_TARGET_CPA = "cpa" | |
| SYNC_TARGET_SUB2API = "sub2api" | |
| _SYNC_TARGET_META = { | |
| SYNC_TARGET_CPA: { | |
| "label": "CPA", | |
| "toggle_key": "SYNC_TARGET_CPA", | |
| "config_keys": ("CPA_URL", "CPA_KEY"), | |
| }, | |
| SYNC_TARGET_SUB2API: { | |
| "label": "Sub2API", | |
| "toggle_key": "SYNC_TARGET_SUB2API", | |
| "config_keys": ("SUB2API_URL", "SUB2API_EMAIL", "SUB2API_PASSWORD"), | |
| }, | |
| } | |
| _TRUE_VALUES = {"1", "true", "yes", "on", "enabled"} | |
| def _normalize_env(env: Mapping[str, object] | None = None) -> dict[str, str]: | |
| source = env or os.environ | |
| return {str(key): "" if value is None else str(value) for key, value in source.items()} | |
| def parse_bool_env(value: object, default: bool = False) -> bool: | |
| if value is None: | |
| return default | |
| text = parse_env_value(str(value)) | |
| if not text: | |
| return default | |
| return text.strip().lower() in _TRUE_VALUES | |
| def get_sync_target_meta(target: str) -> dict[str, object]: | |
| try: | |
| return _SYNC_TARGET_META[target] | |
| except KeyError as exc: | |
| raise KeyError(f"未知同步目标: {target}") from exc | |
| def get_sync_target_states(env: Mapping[str, object] | None = None) -> dict[str, bool]: | |
| values = _normalize_env(env) | |
| states = {} | |
| for target, meta in _SYNC_TARGET_META.items(): | |
| toggle_key = str(meta["toggle_key"]) | |
| config_keys = tuple(meta["config_keys"]) | |
| raw_toggle = (values.get(toggle_key) or "").strip() | |
| if raw_toggle: | |
| states[target] = parse_bool_env(raw_toggle) | |
| else: | |
| states[target] = all((values.get(key) or "").strip() for key in config_keys) | |
| return states | |
| def is_sync_target_enabled(target: str, env: Mapping[str, object] | None = None) -> bool: | |
| return get_sync_target_states(env).get(target, False) | |
| def get_enabled_sync_targets(env: Mapping[str, object] | None = None) -> list[str]: | |
| states = get_sync_target_states(env) | |
| return [target for target in _SYNC_TARGET_META if states.get(target)] | |
| def get_available_sync_targets(env: Mapping[str, object] | None = None) -> list[str]: | |
| values = _normalize_env(env) | |
| available = [] | |
| for target, meta in _SYNC_TARGET_META.items(): | |
| if all((values.get(key) or "").strip() for key in tuple(meta["config_keys"])): | |
| available.append(target) | |
| return available | |
| def get_sync_target_labels(targets: list[str] | None = None) -> list[str]: | |
| if targets is None: | |
| targets = list(_SYNC_TARGET_META) | |
| labels = [] | |
| for target in targets: | |
| meta = _SYNC_TARGET_META.get(target) | |
| if meta: | |
| labels.append(str(meta["label"])) | |
| return labels | |
| def describe_sync_targets(targets: list[str] | None = None) -> str: | |
| labels = get_sync_target_labels(targets) | |
| if not labels: | |
| return "未启用远端同步目标" | |
| return " + ".join(labels) | |
| def get_missing_target_configs( | |
| targets: list[str] | None = None, env: Mapping[str, object] | None = None | |
| ) -> list[tuple[str, str]]: | |
| values = _normalize_env(env) | |
| missing: list[tuple[str, str]] = [] | |
| for target in targets or []: | |
| meta = get_sync_target_meta(target) | |
| for key in tuple(meta["config_keys"]): | |
| value = (values.get(key) or "").strip() | |
| if not value: | |
| missing.append((key, str(meta["label"]))) | |
| return missing | |
| def sync_to_configured_targets(): | |
| results = {} | |
| enabled_targets = get_enabled_sync_targets() | |
| if SYNC_TARGET_CPA in enabled_targets: | |
| from autoteam.cpa_sync import sync_to_cpa | |
| try: | |
| results[SYNC_TARGET_CPA] = sync_to_cpa() | |
| except Exception as exc: | |
| logger.warning("[Sync] CPA 同步失败,保留本地轮换结果: %s", exc) | |
| results[SYNC_TARGET_CPA] = {"ok": False, "error": str(exc)} | |
| if SYNC_TARGET_SUB2API in enabled_targets: | |
| from autoteam.sub2api_sync import sync_to_sub2api | |
| try: | |
| results[SYNC_TARGET_SUB2API] = sync_to_sub2api() | |
| except Exception as exc: | |
| logger.warning("[Sync] Sub2API 同步失败,保留本地轮换结果: %s", exc) | |
| results[SYNC_TARGET_SUB2API] = {"ok": False, "error": str(exc)} | |
| return results | |
| def sync_account_to_configured_targets(email: str, filepath: str): | |
| """只把一个已就绪的账号凭证同步到已启用目标,不触发远端清理。""" | |
| from autoteam.accounts import STATUS_ACTIVE, find_account, is_account_disabled, load_accounts | |
| normalized_email = (email or "").strip().lower() | |
| auth_path = Path(filepath) | |
| if not normalized_email: | |
| return {"ok": False, "skipped": True, "reason": "missing_email"} | |
| if not auth_path.exists(): | |
| logger.warning("[Sync] 新凭证文件不存在,跳过即时同步: %s (%s)", normalized_email, auth_path) | |
| return {"ok": False, "skipped": True, "reason": "missing_auth_file", "auth_file": str(auth_path)} | |
| account = find_account(load_accounts(), normalized_email) | |
| if not account: | |
| logger.warning("[Sync] 未找到账号记录,跳过新凭证即时同步: %s", normalized_email) | |
| return {"ok": False, "skipped": True, "reason": "account_missing", "auth_file": auth_path.name} | |
| if is_account_disabled(account): | |
| logger.info("[Sync] 账号已禁用,跳过新凭证即时同步: %s", normalized_email) | |
| return {"ok": False, "skipped": True, "reason": "account_disabled", "auth_file": auth_path.name} | |
| if account.get("status") != STATUS_ACTIVE: | |
| logger.info( | |
| "[Sync] 账号尚未 active,跳过新凭证即时同步: %s (status=%s)", | |
| normalized_email, | |
| account.get("status"), | |
| ) | |
| return { | |
| "ok": False, | |
| "skipped": True, | |
| "reason": "account_not_active", | |
| "status": account.get("status"), | |
| "auth_file": auth_path.name, | |
| } | |
| account_auth = account.get("auth_file") | |
| if account_auth and Path(account_auth).name != auth_path.name: | |
| logger.info( | |
| "[Sync] 请求同步的凭证与账号当前 auth_file 不同,仍按请求文件上传: %s (%s -> %s)", | |
| normalized_email, | |
| Path(account_auth).name, | |
| auth_path.name, | |
| ) | |
| results = {} | |
| enabled_targets = get_enabled_sync_targets() | |
| if SYNC_TARGET_CPA in enabled_targets: | |
| from autoteam.cpa_sync import upload_to_cpa | |
| try: | |
| uploaded = upload_to_cpa(auth_path) | |
| results[SYNC_TARGET_CPA] = {"ok": bool(uploaded), "uploaded": auth_path.name} | |
| except Exception as exc: | |
| logger.warning("[Sync] CPA 新凭证即时同步失败,保留本地结果: %s", exc) | |
| results[SYNC_TARGET_CPA] = {"ok": False, "error": str(exc), "uploaded": auth_path.name} | |
| if SYNC_TARGET_SUB2API in enabled_targets: | |
| from autoteam.sub2api_sync import sync_account_to_sub2api | |
| try: | |
| quota_info = account.get("last_quota") if isinstance(account.get("last_quota"), dict) else None | |
| target_result = sync_account_to_sub2api(normalized_email, str(auth_path), quota_info=quota_info) | |
| results[SYNC_TARGET_SUB2API] = {"ok": True, **target_result} | |
| except Exception as exc: | |
| logger.warning("[Sync] Sub2API 新凭证即时同步失败,保留本地结果: %s", exc) | |
| results[SYNC_TARGET_SUB2API] = {"ok": False, "error": str(exc), "uploaded": auth_path.name} | |
| if not results: | |
| logger.info("[Sync] 未启用远端同步目标,跳过新凭证即时同步: %s", normalized_email) | |
| return {"ok": True, "skipped": True, "reason": "no_enabled_targets", "auth_file": auth_path.name} | |
| return { | |
| "ok": all(item.get("ok") for item in results.values()), | |
| "auth_file": auth_path.name, | |
| "targets": results, | |
| } | |
| def sync_main_codex_to_configured_targets(filepath: str): | |
| results = {} | |
| enabled_targets = get_enabled_sync_targets() | |
| if SYNC_TARGET_CPA in enabled_targets: | |
| from autoteam.cpa_sync import sync_main_codex_to_cpa | |
| results[SYNC_TARGET_CPA] = sync_main_codex_to_cpa(filepath) | |
| if SYNC_TARGET_SUB2API in enabled_targets: | |
| from autoteam.sub2api_sync import sync_main_codex_to_sub2api | |
| results[SYNC_TARGET_SUB2API] = sync_main_codex_to_sub2api(filepath) | |
| return results | |
| def delete_main_codex_from_configured_targets(*, include_disabled: bool = False): | |
| results = {} | |
| targets = get_available_sync_targets() if include_disabled else get_enabled_sync_targets() | |
| if SYNC_TARGET_CPA in targets: | |
| from autoteam.cpa_sync import delete_main_codex_from_cpa | |
| results[SYNC_TARGET_CPA] = delete_main_codex_from_cpa() | |
| if SYNC_TARGET_SUB2API in targets: | |
| from autoteam.sub2api_sync import delete_main_codex_from_sub2api | |
| results[SYNC_TARGET_SUB2API] = delete_main_codex_from_sub2api() | |
| return results | |
| def delete_account_from_configured_targets( | |
| email: str, *, auth_names: list[str] | None = None, include_disabled: bool = False | |
| ): | |
| results = {} | |
| targets = get_available_sync_targets() if include_disabled else get_enabled_sync_targets() | |
| if SYNC_TARGET_CPA in targets: | |
| from autoteam.cpa_sync import delete_from_cpa, list_cpa_files | |
| deleted = [] | |
| auth_name_set = set(auth_names or []) | |
| for item in list_cpa_files(): | |
| item_email = (item.get("email") or "").lower() | |
| item_name = item.get("name") or "" | |
| if item_email == email.lower() or item_name in auth_name_set: | |
| if delete_from_cpa(item_name): | |
| deleted.append(item_name) | |
| results[SYNC_TARGET_CPA] = {"deleted": deleted, "count": len(deleted)} | |
| if SYNC_TARGET_SUB2API in targets: | |
| from autoteam.sub2api_sync import delete_account_from_sub2api | |
| results[SYNC_TARGET_SUB2API] = delete_account_from_sub2api(email, auth_names=auth_names or []) | |
| return results | |