"""Sub2API 账号同步。""" from __future__ import annotations import base64 import json import logging import time from datetime import datetime, timezone from pathlib import Path import requests from autoteam.codex_auth import CODEX_CLIENT_ID from autoteam.config import ( SUB2API_AUTO_PAUSE_ON_EXPIRED, SUB2API_CONCURRENCY, SUB2API_EMAIL, SUB2API_GROUP, SUB2API_MODEL_WHITELIST, SUB2API_OPENAI_PASSTHROUGH, SUB2API_OPENAI_WS_MODE, SUB2API_OVERWRITE_ACCOUNT_SETTINGS, SUB2API_PASSWORD, SUB2API_PRIORITY, SUB2API_PROXY, SUB2API_RATE_MULTIPLIER, SUB2API_URL, ) from autoteam.textio import read_text logger = logging.getLogger(__name__) _TIMEOUT = 10 _PAGE_SIZE = 200 _EXTRA_MANAGED = "autoteam_managed" _EXTRA_KIND = "autoteam_kind" _EXTRA_EMAIL = "autoteam_email" _EXTRA_AUTH_FILE = "autoteam_auth_file" _EXTRA_SOURCE = "autoteam_source" _EXTRA_LAST_SYNC_AT = "autoteam_last_sync_at" _EXTRA_GROUP_IDS = "autoteam_sub2api_group_ids" _EXTRA_GROUP_NAMES = "autoteam_sub2api_group_names" _KIND_POOL = "pool" _KIND_MAIN = "main" _REMOTE_AUTH_FILE_PREFIX = "sub2api-" def _excerpt(text: str | bytes | None, limit: int = 200) -> str: value = text.decode("utf-8", errors="ignore") if isinstance(text, bytes) else str(text or "") value = value.strip().replace("\n", " ") if len(value) > limit: value = value[:limit] + "..." return value def _api_base_url() -> str: base = (SUB2API_URL or "").strip().rstrip("/") if base.endswith("/api/v1"): return base if base.endswith("/api"): return f"{base}/v1" return f"{base}/api/v1" def _request(method: str, path: str, *, token: str | None = None, label: str, **kwargs): url = f"{_api_base_url()}{path}" headers = dict(kwargs.pop("headers", {}) or {}) if token: headers["Authorization"] = f"Bearer {token}" resp = requests.request(method, url, headers=headers, timeout=_TIMEOUT, **kwargs) try: payload = resp.json() except Exception as exc: raise RuntimeError(f"[Sub2API] {label}返回了非 JSON 内容: {_excerpt(resp.text)}") from exc if resp.status_code != 200: message = payload.get("message") or payload.get("detail") or _excerpt(resp.text) raise RuntimeError(f"[Sub2API] {label}失败: HTTP {resp.status_code} {message}") if payload.get("code", 0) != 0: message = payload.get("message") or _excerpt(resp.text) raise RuntimeError(f"[Sub2API] {label}失败: {message}") return payload.get("data") def _login() -> str: data = _request( "POST", "/auth/login", label="管理员登录", json={"email": SUB2API_EMAIL, "password": SUB2API_PASSWORD}, ) if not isinstance(data, dict): raise RuntimeError("[Sub2API] 管理员登录返回格式异常") if data.get("requires_2fa"): raise RuntimeError("[Sub2API] 当前账号启用了 2FA,AutoTeam 暂不支持自动同步到 Sub2API") access_token = (data.get("access_token") or "").strip() if not access_token: raise RuntimeError("[Sub2API] 管理员登录成功但未返回 access_token") return access_token def _list_openai_oauth_accounts(token: str) -> list[dict]: items = [] page = 1 while True: data = _request( "GET", "/admin/accounts", token=token, label="获取账号列表", params={ "page": page, "page_size": _PAGE_SIZE, "platform": "openai", "type": "oauth", "sort_by": "id", "sort_order": "asc", }, ) page_items = data.get("items") if isinstance(data, dict) else None if not isinstance(page_items, list): break items.extend(page_items) total = int(data.get("total") or 0) if isinstance(data, dict) else 0 if not page_items or len(items) >= total: break page += 1 return items def _is_managed_account(item: dict, *, kind: str | None = None) -> bool: extra = item.get("extra") or {} if not isinstance(extra, dict): return False if extra.get(_EXTRA_SOURCE) != "autoteam" and not extra.get(_EXTRA_MANAGED): return False if kind and extra.get(_EXTRA_KIND) != kind: return False return True def _managed_email(item: dict) -> str: extra = item.get("extra") or {} credentials = item.get("credentials") or {} return (extra.get(_EXTRA_EMAIL) or credentials.get("email") or item.get("name") or "").strip().lower() def _managed_auth_file(item: dict) -> str: extra = item.get("extra") or {} return (extra.get(_EXTRA_AUTH_FILE) or "").strip() def _managed_group_ids(item: dict) -> list[int]: extra = item.get("extra") or {} values = extra.get(_EXTRA_GROUP_IDS) if not isinstance(values, list): return [] result = [] seen = set() for value in values: try: group_id = int(value) except (TypeError, ValueError): continue if group_id <= 0 or group_id in seen: continue seen.add(group_id) result.append(group_id) return result def _remote_auth_file_name(local_name: str) -> str: value = str(local_name or "").strip() if not value: return "" if value.startswith(_REMOTE_AUTH_FILE_PREFIX): return value return f"{_REMOTE_AUTH_FILE_PREFIX}{value}" def _remote_auth_file_candidates(names: list[str] | None) -> set[str]: candidates = set() for name in names or []: value = str(name or "").strip() if not value: continue candidates.add(value) candidates.add(_remote_auth_file_name(value)) return candidates def _split_group_spec(value: str | None) -> list[str]: text = str(value or "").replace(",", ",") parts = [] seen = set() for raw in text.replace("\n", ",").split(","): item = raw.strip() if not item: continue lowered = item.lower() if lowered in seen: continue seen.add(lowered) parts.append(item) return parts def _split_csv_spec(value: str | None) -> list[str]: text = str(value or "").replace(",", ",") parts = [] seen = set() for raw in text.replace("\n", ",").split(","): item = raw.strip() if not item: continue lowered = item.lower() if lowered in seen: continue seen.add(lowered) parts.append(item) return parts def _build_managed_model_mapping(model_whitelist: str | None = None) -> dict[str, str] | None: models = _split_csv_spec(model_whitelist if model_whitelist is not None else SUB2API_MODEL_WHITELIST) if not models: return None return {model: model for model in models} def _build_account_settings() -> dict: return { "concurrency": SUB2API_CONCURRENCY, "priority": SUB2API_PRIORITY, "rate_multiplier": SUB2API_RATE_MULTIPLIER, "auto_pause_on_expired": SUB2API_AUTO_PAUSE_ON_EXPIRED, } def _apply_managed_credentials_settings(credentials: dict, *, model_whitelist: str | None = None) -> dict: model_mapping = _build_managed_model_mapping(model_whitelist) if model_mapping: credentials["model_mapping"] = model_mapping return credentials def _apply_managed_extra_settings(extra: dict) -> dict: extra["openai_oauth_responses_websockets_v2_mode"] = SUB2API_OPENAI_WS_MODE extra["openai_oauth_responses_websockets_v2_enabled"] = SUB2API_OPENAI_WS_MODE != "off" if SUB2API_OPENAI_PASSTHROUGH: extra["openai_passthrough"] = True else: extra.pop("openai_passthrough", None) extra.pop("openai_oauth_passthrough", None) return extra def _dedupe_managed_accounts(token: str, items: list[dict], *, kind: str) -> tuple[dict[str, dict], int]: deduped: dict[str, dict] = {} duplicates_deleted = 0 for item in items: if not _is_managed_account(item, kind=kind): continue key = _managed_email(item) if not key: key = f"{kind}:{item.get('id')}" previous = deduped.get(key) if previous is None or int(item.get("id") or 0) > int(previous.get("id") or 0): if previous is not None: _delete_account(token, previous, label="删除重复账号") duplicates_deleted += 1 deduped[key] = item else: _delete_account(token, item, label="删除重复账号") duplicates_deleted += 1 return deduped, duplicates_deleted def _select_managed_account_by_email(items: list[dict], email: str, *, kind: str) -> tuple[dict | None, int]: matches = [ item for item in items if _is_managed_account(item, kind=kind) and _managed_email(item) == email.lower() ] if not matches: return None, 0 matches.sort(key=lambda item: int(item.get("id") or 0), reverse=True) return matches[0], max(0, len(matches) - 1) def _parse_jwt_payload(token: str) -> dict: parts = (token or "").split(".") if len(parts) < 2: return {} payload = parts[1] payload += "=" * (-len(payload) % 4) try: return json.loads(base64.urlsafe_b64decode(payload)) except Exception: return {} def _list_openai_groups(token: str) -> list[dict]: data = _request( "GET", "/admin/groups/all", token=token, label="获取 Sub2API 分组列表", params={"platform": "openai"}, ) return data if isinstance(data, list) else [] def _get_group_by_id(token: str, group_id: int) -> dict | None: try: data = _request( "GET", f"/admin/groups/{group_id}", token=token, label=f"获取 Sub2API 分组 {group_id}", ) except Exception: return None return data if isinstance(data, dict) else None def _resolve_group_binding(token: str, group_spec: str | None = None) -> tuple[list[int], list[str]]: parts = _split_group_spec(group_spec if group_spec is not None else SUB2API_GROUP) if not parts: return [], [] groups = _list_openai_groups(token) by_id = {} by_name = {} for item in groups: if not isinstance(item, dict): continue try: group_id = int(item.get("id") or 0) except (TypeError, ValueError): continue if group_id <= 0: continue by_id[group_id] = item name = str(item.get("name") or "").strip() if name: by_name[name.lower()] = item resolved_ids = [] resolved_names = [] seen_ids = set() for part in parts: group = None if part.isdigit(): group = by_id.get(int(part)) or _get_group_by_id(token, int(part)) else: group = by_name.get(part.lower()) if not isinstance(group, dict): raise RuntimeError(f"[Sub2API] 未找到分组: {part}") platform = str(group.get("platform") or "").strip().lower() if platform and platform != "openai": raise RuntimeError(f"[Sub2API] 分组 {part} 不是 openai 平台,当前平台: {platform}") try: group_id = int(group.get("id") or 0) except (TypeError, ValueError): group_id = 0 if group_id <= 0: raise RuntimeError(f"[Sub2API] 分组 {part} 缺少有效 ID") if group_id in seen_ids: continue seen_ids.add(group_id) resolved_ids.append(group_id) resolved_names.append(str(group.get("name") or group_id)) return resolved_ids, resolved_names def _list_proxies(token: str) -> list[dict]: data = _request( "GET", "/admin/proxies/all", token=token, label="获取 Sub2API 代理列表", ) return data if isinstance(data, list) else [] def _resolve_proxy_id(token: str, proxy_spec: str | None = None) -> int | None: spec = str(proxy_spec if proxy_spec is not None else SUB2API_PROXY or "").strip() if not spec: return None if spec.lstrip("+-").isdigit(): proxy_id = int(spec) if proxy_id <= 0: raise RuntimeError(f"[Sub2API] 代理 ID 必须是正整数: {spec}") return proxy_id proxies = _list_proxies(token) matches = [] for item in proxies: if not isinstance(item, dict): continue name = str(item.get("name") or "").strip() if name.lower() == spec.lower(): matches.append(item) if not matches: raise RuntimeError(f"[Sub2API] 未找到代理: {spec}") if len(matches) > 1: raise RuntimeError(f"[Sub2API] 找到多个同名代理: {spec}") try: proxy_id = int(matches[0].get("id") or 0) except (TypeError, ValueError): proxy_id = 0 if proxy_id <= 0: raise RuntimeError(f"[Sub2API] 代理 {spec} 缺少有效 ID") return proxy_id def _extract_organization_id(auth_claims: dict) -> str: organizations = auth_claims.get("organizations") if not isinstance(organizations, list): return "" for item in organizations: if not isinstance(item, dict): continue if item.get("is_default") and item.get("id"): return str(item["id"]) for item in organizations: if isinstance(item, dict) and item.get("id"): return str(item["id"]) return "" def _parse_timestamp(value, *, default: int | None = None) -> int | None: if isinstance(value, (int, float)): return int(value) text = str(value or "").strip() if not text: return default try: return int(float(text)) except Exception: pass try: if text.endswith("Z"): return int(datetime.fromisoformat(text.replace("Z", "+00:00")).timestamp()) return int(datetime.fromisoformat(text).timestamp()) except Exception: return default def _to_local_iso(ts: int | None) -> str: if not ts: return "" return datetime.fromtimestamp(int(ts), timezone.utc).astimezone().isoformat(timespec="seconds") def _quota_extra_fields(quota_info: dict | None, *, now_ts: int | None = None) -> dict: if not isinstance(quota_info, dict): return {} now_ts = int(now_ts or time.time()) primary_pct = int(quota_info.get("primary_pct", 0) or 0) weekly_pct = int(quota_info.get("weekly_pct", 0) or 0) primary_resets_at = _parse_timestamp(quota_info.get("primary_resets_at")) weekly_resets_at = _parse_timestamp(quota_info.get("weekly_resets_at")) extra = { "codex_5h_used_percent": primary_pct, "codex_5h_window_minutes": 300, "codex_7d_used_percent": weekly_pct, "codex_7d_window_minutes": 10080, "codex_primary_used_percent": primary_pct, "codex_primary_window_minutes": 300, "codex_secondary_used_percent": weekly_pct, "codex_secondary_window_minutes": 10080, "codex_primary_over_secondary_percent": 0, "codex_usage_updated_at": _to_local_iso(now_ts), } if primary_resets_at: primary_after = max(0, primary_resets_at - now_ts) extra.update( { "codex_5h_reset_after_seconds": primary_after, "codex_5h_reset_at": _to_local_iso(primary_resets_at), "codex_primary_reset_after_seconds": primary_after, } ) if weekly_resets_at: weekly_after = max(0, weekly_resets_at - now_ts) extra.update( { "codex_7d_reset_after_seconds": weekly_after, "codex_7d_reset_at": _to_local_iso(weekly_resets_at), "codex_secondary_reset_after_seconds": weekly_after, } ) return extra def _load_auth_data(path: Path) -> dict: return json.loads(read_text(path)) def _build_credentials(auth_data: dict) -> dict: id_token = auth_data.get("id_token", "") claims = _parse_jwt_payload(id_token) if id_token else {} auth_claims = claims.get("https://api.openai.com/auth", {}) if isinstance(claims, dict) else {} credentials = {"access_token": auth_data.get("access_token", "")} expires_at = _parse_timestamp(auth_data.get("expired"), default=int(time.time()) + 3600) if expires_at: credentials["expires_at"] = expires_at refresh_token = auth_data.get("refresh_token", "") if refresh_token: credentials["refresh_token"] = refresh_token if id_token: credentials["id_token"] = id_token client_id = auth_data.get("client_id") or claims.get("aud", [""])[0] if isinstance(claims.get("aud"), list) else "" client_id = client_id or CODEX_CLIENT_ID if client_id: credentials["client_id"] = client_id email = auth_data.get("email") or claims.get("email") or "" if email: credentials["email"] = email account_id = auth_data.get("account_id") or auth_claims.get("chatgpt_account_id") or "" if account_id: credentials["chatgpt_account_id"] = account_id user_id = auth_claims.get("chatgpt_user_id") or "" if user_id: credentials["chatgpt_user_id"] = user_id organization_id = auth_claims.get("poid") or _extract_organization_id(auth_claims) if organization_id: credentials["organization_id"] = organization_id plan_type = auth_claims.get("chatgpt_plan_type") or "" if plan_type: credentials["plan_type"] = plan_type subscription_expires_at = auth_claims.get("chatgpt_subscription_active_until") or "" if subscription_expires_at: credentials["subscription_expires_at"] = subscription_expires_at return credentials def _build_extra(email: str, auth_file_name: str, *, kind: str, quota_info: dict | None = None) -> dict: extra = { _EXTRA_MANAGED: True, _EXTRA_KIND: kind, _EXTRA_EMAIL: email.lower(), _EXTRA_AUTH_FILE: _remote_auth_file_name(auth_file_name), _EXTRA_SOURCE: "autoteam", _EXTRA_LAST_SYNC_AT: int(time.time()), "email": email.lower(), } extra.update(_quota_extra_fields(quota_info)) return extra def _attach_group_metadata(extra: dict, group_ids: list[int] | None, group_names: list[str] | None) -> dict: extra[_EXTRA_GROUP_IDS] = [int(value) for value in group_ids or []] extra[_EXTRA_GROUP_NAMES] = [str(value) for value in group_names or [] if str(value).strip()] return extra def _account_group_ids(account: dict) -> list[int]: result = [] seen = set() for value in account.get("group_ids") or []: try: group_id = int(value) except (TypeError, ValueError): continue if group_id <= 0 or group_id in seen: continue seen.add(group_id) result.append(group_id) for item in account.get("groups") or []: if not isinstance(item, dict): continue try: group_id = int(item.get("id") or 0) except (TypeError, ValueError): continue if group_id <= 0 or group_id in seen: continue seen.add(group_id) result.append(group_id) return result def _merge_group_ids(account: dict, desired_group_ids: list[int] | None) -> list[int]: desired = set() for value in desired_group_ids or []: try: group_id = int(value) except (TypeError, ValueError): continue if group_id > 0: desired.add(group_id) existing = set(_account_group_ids(account)) previous_managed = set(_managed_group_ids(account)) merged = (existing - previous_managed) | desired return sorted(merged) def _create_account( token: str, *, name: str, credentials: dict, extra: dict, label: str, group_ids: list[int] | None = None, account_settings: dict | None = None, proxy_id: int | None = None, ) -> dict: payload = { "name": name, "platform": "openai", "type": "oauth", "credentials": credentials, "extra": extra, **_build_account_settings(), "group_ids": list(group_ids or []), } if proxy_id is not None: payload["proxy_id"] = int(proxy_id) if account_settings: payload.update(account_settings) return _request( "POST", "/admin/accounts", token=token, label=label, json=payload, ) def _update_account( token: str, account: dict, *, credentials: dict, extra: dict, name: str | None = None, status: str | None = None, group_ids: list[int] | None = None, account_settings: dict | None = None, ): payload = {"credentials": credentials, "extra": extra} if name: payload["name"] = name if status: payload["status"] = status if group_ids is not None: payload["group_ids"] = list(group_ids) if account_settings: payload.update(account_settings) return _request( "PUT", f"/admin/accounts/{account['id']}", token=token, label=f"更新账号 {name or _managed_email(account) or account.get('id')}", json=payload, ) def _upsert_managed_pool_account( token: str, *, target: dict, existing: dict | None, group_ids: list[int], group_names: list[str], overwrite_account_settings: bool, proxy_id: int | None = None, ) -> dict: email = target["email"] desired_credentials = _build_credentials(target["auth_data"]) desired_extra = _build_extra( email, target["auth_path"].name, kind=_KIND_POOL, quota_info=target.get("quota_info"), ) _attach_group_metadata(desired_extra, group_ids, group_names) if existing: merged_credentials = dict(existing.get("credentials") or {}) merged_credentials.update(desired_credentials) merged_extra = dict(existing.get("extra") or {}) merged_extra.update(desired_extra) account_settings = None if overwrite_account_settings: account_settings = _build_account_settings() _apply_managed_credentials_settings(merged_credentials) _apply_managed_extra_settings(merged_extra) _update_account( token, existing, credentials=merged_credentials, extra=merged_extra, status="active" if existing.get("status") != "active" else None, group_ids=_merge_group_ids(existing, group_ids), account_settings=account_settings, ) logger.info("[Sub2API] 更新: %s", email) return {"action": "updated", "account_id": existing.get("id")} _apply_managed_credentials_settings(desired_credentials) _apply_managed_extra_settings(desired_extra) created = _create_account( token, name=target["name"], credentials=desired_credentials, extra=desired_extra, label=f"创建账号 {email}", group_ids=group_ids, account_settings=_build_account_settings(), proxy_id=proxy_id, ) logger.info("[Sub2API] 创建: %s", email) account_id = created.get("id") if isinstance(created, dict) else None return {"action": "created", "account_id": account_id} def _delete_account(token: str, account: dict, *, label: str = "删除账号") -> bool: _request( "DELETE", f"/admin/accounts/{account['id']}", token=token, label=f"{label} {account.get('name') or account.get('id')}", ) return True def verify_sub2api_connection() -> bool: try: token = _login() accounts = _list_openai_oauth_accounts(token) group_ids, group_names = _resolve_group_binding(token) if group_ids: logger.info( "[验证] Sub2API 连接成功(当前 %d 个 OpenAI OAuth 账号,分组: %s)", len(accounts), ", ".join(f"{name}#{group_id}" for group_id, name in zip(group_ids, group_names)), ) else: logger.info("[验证] Sub2API 连接成功(当前 %d 个 OpenAI OAuth 账号)", len(accounts)) return True except Exception as exc: logger.error("[验证] Sub2API 连接失败: %s", exc) return False def sync_to_sub2api(): from autoteam.accounts import STATUS_ACTIVE, is_account_disabled, load_accounts accounts = load_accounts() local_emails = {str(acc.get("email") or "").lower() for acc in accounts if acc.get("email")} active_targets = {} for acc in accounts: if is_account_disabled(acc): continue if acc.get("status") != STATUS_ACTIVE or not acc.get("auth_file"): continue auth_path = Path(acc["auth_file"]) if not auth_path.exists(): continue try: auth_data = _load_auth_data(auth_path) except Exception as exc: logger.warning("[Sub2API] 读取 auth 文件失败,跳过 %s: %s", auth_path, exc) continue email = (auth_data.get("email") or acc.get("email") or "").strip().lower() if not email: continue active_targets[email] = { "email": email, "name": acc.get("email") or email, "auth_path": auth_path, "auth_data": auth_data, "quota_info": acc.get("last_quota"), } token = _login() group_ids, group_names = _resolve_group_binding(token) remote_accounts = _list_openai_oauth_accounts(token) existing_by_email, duplicates_deleted = _dedupe_managed_accounts(token, remote_accounts, kind=_KIND_POOL) logger.info( "[Sub2API] active 账号: %d, Sub2API 管理账号: %d", len(active_targets), len(existing_by_email), ) if group_ids: logger.info( "[Sub2API] 目标分组: %s", ", ".join(f"{name}#{group_id}" for group_id, name in zip(group_ids, group_names)) ) created = 0 updated = 0 deleted = 0 overwrite_account_settings = SUB2API_OVERWRITE_ACCOUNT_SETTINGS proxy_id = None proxy_id_resolved = False for email, target in active_targets.items(): existing = existing_by_email.get(email) if not existing and not proxy_id_resolved: proxy_id = _resolve_proxy_id(token) proxy_id_resolved = True result = _upsert_managed_pool_account( token, target=target, existing=existing, group_ids=group_ids, group_names=group_names, overwrite_account_settings=overwrite_account_settings, proxy_id=proxy_id, ) if result["action"] == "created": created += 1 else: updated += 1 for email, account in existing_by_email.items(): if email in local_emails and email not in active_targets: _delete_account(token, account, label="删除非 active 账号") logger.info("[Sub2API] 删除非 active 账号: %s", email) deleted += 1 final_accounts = _list_openai_oauth_accounts(token) final_managed = [item for item in final_accounts if _is_managed_account(item, kind=_KIND_POOL)] logger.info( "[Sub2API] 同步完成: 创建 %d, 更新 %d, 删除 %d, 远端去重 %d", created, updated, deleted, duplicates_deleted, ) logger.info("[Sub2API] Sub2API 中本地管理: %d, 本地 active: %d", len(final_managed), len(active_targets)) return { "created": created, "updated": updated, "deleted": deleted, "remote_duplicates_deleted": duplicates_deleted, } def sync_account_to_sub2api(email: str, filepath: str, *, quota_info: dict | None = None): auth_path = Path(filepath) if not auth_path.exists(): raise FileNotFoundError(f"账号认证文件不存在: {auth_path}") auth_data = _load_auth_data(auth_path) email_hint = (email or "").strip().lower() auth_email = (auth_data.get("email") or "").strip().lower() target_email = auth_email or email_hint if not target_email: raise RuntimeError("[Sub2API] 账号认证文件中未找到邮箱") if email_hint and auth_email and email_hint != auth_email: logger.warning("[Sub2API] 认证文件邮箱与请求邮箱不一致,按认证文件邮箱同步: %s -> %s", email_hint, auth_email) token = _login() group_ids, group_names = _resolve_group_binding(token) remote_accounts = _list_openai_oauth_accounts(token) existing, duplicates_seen = _select_managed_account_by_email(remote_accounts, target_email, kind=_KIND_POOL) proxy_id = None if existing else _resolve_proxy_id(token) target = { "email": target_email, "name": target_email, "auth_path": auth_path, "auth_data": auth_data, "quota_info": quota_info, } result = _upsert_managed_pool_account( token, target=target, existing=existing, group_ids=group_ids, group_names=group_names, overwrite_account_settings=SUB2API_OVERWRITE_ACCOUNT_SETTINGS, proxy_id=proxy_id, ) remote_auth_name = _remote_auth_file_name(auth_path.name) logger.info( "[Sub2API] 单凭证同步完成: %s (%s, account_id=%s, duplicates_seen=%d)", remote_auth_name, result["action"], result.get("account_id"), duplicates_seen, ) return { "uploaded": remote_auth_name, "action": result["action"], "account_id": result.get("account_id"), "remote_duplicates_seen": duplicates_seen, } def sync_main_codex_to_sub2api(filepath): auth_path = Path(filepath) if not auth_path.exists(): raise FileNotFoundError(f"主号认证文件不存在: {auth_path}") auth_data = _load_auth_data(auth_path) email = (auth_data.get("email") or "").strip().lower() token = _login() group_ids, group_names = _resolve_group_binding(token) remote_accounts = _list_openai_oauth_accounts(token) existing_by_email, duplicates_deleted = _dedupe_managed_accounts(token, remote_accounts, kind=_KIND_MAIN) desired_credentials = _build_credentials(auth_data) desired_extra = _build_extra(email, auth_path.name, kind=_KIND_MAIN) _attach_group_metadata(desired_extra, group_ids, group_names) name = f"AutoTeam Main | {email}" if email else "AutoTeam Main" overwrite_account_settings = SUB2API_OVERWRITE_ACCOUNT_SETTINGS current = existing_by_email.get(email) if email else None if current: merged_credentials = dict(current.get("credentials") or {}) merged_credentials.update(desired_credentials) merged_extra = dict(current.get("extra") or {}) merged_extra.update(desired_extra) account_settings = None if overwrite_account_settings: account_settings = _build_account_settings() _apply_managed_credentials_settings(merged_credentials) _apply_managed_extra_settings(merged_extra) _update_account( token, current, credentials=merged_credentials, extra=merged_extra, name=name, status="active", group_ids=_merge_group_ids(current, group_ids), account_settings=account_settings, ) account_id = current.get("id") else: _apply_managed_credentials_settings(desired_credentials) _apply_managed_extra_settings(desired_extra) created = _create_account( token, name=name, credentials=desired_credentials, extra=desired_extra, label="创建主号账号", group_ids=group_ids, account_settings=_build_account_settings(), ) account_id = created.get("id") if isinstance(created, dict) else None deleted = [] for item in existing_by_email.values(): if current and item.get("id") == current.get("id"): continue if email and _managed_email(item) == email: continue _delete_account(token, item, label="删除旧主号账号") deleted.append(item.get("id")) remote_auth_name = _remote_auth_file_name(auth_path.name) logger.info( "[Sub2API] 主号 Codex 已同步: %s (account_id=%s, duplicates=%d, deleted_old=%d)", remote_auth_name, account_id, duplicates_deleted, len(deleted), ) return {"uploaded": remote_auth_name, "account_id": account_id, "deleted_old": deleted} def delete_main_codex_from_sub2api(): token = _login() remote_accounts = _list_openai_oauth_accounts(token) deleted = [] for item in remote_accounts: if not _is_managed_account(item, kind=_KIND_MAIN): continue _delete_account(token, item, label="删除主号账号") deleted.append(item.get("name") or str(item.get("id"))) return {"deleted": deleted, "count": len(deleted)} def delete_account_from_sub2api(email: str, *, auth_names: list[str] | None = None): token = _login() remote_accounts = _list_openai_oauth_accounts(token) auth_name_set = _remote_auth_file_candidates(auth_names) deleted = [] for item in remote_accounts: if not _is_managed_account(item, kind=_KIND_POOL): continue item_email = _managed_email(item) item_auth_name = _managed_auth_file(item) if item_email != email.lower() and item_auth_name not in auth_name_set: continue _delete_account(token, item, label="删除账号") deleted.append(item.get("name") or str(item.get("id"))) return {"deleted": deleted, "count": len(deleted)}