AutoTeam-F / src /autoteam /sync_targets.py
ZRainbow's picture
feat: AutoTeam 大版本升级:安全轮换、并行、IPv6、指纹加固与前端重构
1f4cfcb
Raw
History Blame Contribute Delete
10.2 kB
"""统一远端同步目标分发: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