|
4 | 4 |
|
5 | 5 | import json |
6 | 6 | import logging |
| 7 | +import base64 |
7 | 8 | from typing import List, Dict, Any, Tuple, Optional |
8 | 9 | from datetime import datetime |
9 | 10 | from urllib.parse import quote |
|
14 | 15 | from ...database.session import get_db |
15 | 16 | from ...database.models import Account |
16 | 17 | from ...config.settings import get_settings |
17 | | -from ..timezone_utils import utcnow_naive |
| 18 | +from ..timezone_utils import UTC, SHANGHAI_TZ, utcnow_naive |
18 | 19 |
|
19 | 20 | logger = logging.getLogger(__name__) |
20 | 21 |
|
21 | 22 |
|
| 23 | +def _decode_jwt_payload_unverified(token: str) -> Dict[str, Any]: |
| 24 | + text = str(token or "").strip() |
| 25 | + if not text or text.count(".") < 2: |
| 26 | + return {} |
| 27 | + try: |
| 28 | + payload_b64 = text.split(".")[1] |
| 29 | + padding = "=" * (-len(payload_b64) % 4) |
| 30 | + raw = base64.urlsafe_b64decode((payload_b64 + padding).encode("utf-8")) |
| 31 | + data = json.loads(raw.decode("utf-8")) |
| 32 | + return data if isinstance(data, dict) else {} |
| 33 | + except Exception: |
| 34 | + return {} |
| 35 | + |
| 36 | + |
| 37 | +def _parse_unix_ts_to_naive_utc(value: Any) -> Optional[datetime]: |
| 38 | + try: |
| 39 | + if value in (None, ""): |
| 40 | + return None |
| 41 | + ts = int(value) |
| 42 | + return datetime.fromtimestamp(ts, tz=UTC).replace(tzinfo=None) |
| 43 | + except Exception: |
| 44 | + return None |
| 45 | + |
| 46 | + |
| 47 | +def _parse_iso_to_naive_utc(value: Any) -> Optional[datetime]: |
| 48 | + try: |
| 49 | + text = str(value or "").strip() |
| 50 | + if not text: |
| 51 | + return None |
| 52 | + dt = datetime.fromisoformat(text.replace("Z", "+00:00")) |
| 53 | + if dt.tzinfo is None: |
| 54 | + return dt |
| 55 | + return dt.astimezone(UTC).replace(tzinfo=None) |
| 56 | + except Exception: |
| 57 | + return None |
| 58 | + |
| 59 | + |
| 60 | +def _format_cpa_datetime(dt: Optional[datetime]) -> str: |
| 61 | + if dt is None: |
| 62 | + return "" |
| 63 | + if dt.tzinfo is None: |
| 64 | + dt = dt.replace(tzinfo=UTC) |
| 65 | + else: |
| 66 | + dt = dt.astimezone(UTC) |
| 67 | + return dt.astimezone(SHANGHAI_TZ).strftime("%Y-%m-%dT%H:%M:%S+08:00") |
| 68 | + |
| 69 | + |
22 | 70 | def _normalize_cpa_auth_files_url(api_url: str) -> str: |
23 | 71 | """将用户填写的 CPA 地址规范化为 auth-files 接口地址。""" |
24 | 72 | normalized = (api_url or "").strip().rstrip("/") |
@@ -100,15 +148,67 @@ def generate_token_json(account: Account) -> dict: |
100 | 148 | Returns: |
101 | 149 | CPA 格式的 Token 字典 |
102 | 150 | """ |
| 151 | + access_token = str(account.access_token or "").strip() |
| 152 | + id_token_raw = str(account.id_token or "").strip() |
| 153 | + # 兼容历史下游:部分工具只从 id_token 提取 chatgpt_account_id。 |
| 154 | + # 当注册链路仅拿到 access_token 时,使用 access_token 作为兜底 id_token 供解析。 |
| 155 | + id_token = id_token_raw or access_token |
| 156 | + refresh_token = str(account.refresh_token or "").strip() |
| 157 | + |
| 158 | + claims = _decode_jwt_payload_unverified(access_token) or _decode_jwt_payload_unverified(id_token) |
| 159 | + auth_claims = claims.get("https://api.openai.com/auth") if isinstance(claims, dict) else {} |
| 160 | + auth_claims = auth_claims if isinstance(auth_claims, dict) else {} |
| 161 | + profile_claims = claims.get("https://api.openai.com/profile") if isinstance(claims, dict) else {} |
| 162 | + profile_claims = profile_claims if isinstance(profile_claims, dict) else {} |
| 163 | + |
| 164 | + account_id = str( |
| 165 | + account.account_id |
| 166 | + or auth_claims.get("chatgpt_account_id") |
| 167 | + or claims.get("chatgpt_account_id") |
| 168 | + or "" |
| 169 | + ).strip() |
| 170 | + workspace_id = str(account.workspace_id or account_id).strip() |
| 171 | + chatgpt_user_id = str( |
| 172 | + auth_claims.get("chatgpt_user_id") |
| 173 | + or auth_claims.get("user_id") |
| 174 | + or claims.get("chatgpt_user_id") |
| 175 | + or claims.get("user_id") |
| 176 | + or "" |
| 177 | + ).strip() |
| 178 | + |
| 179 | + email = str( |
| 180 | + account.email |
| 181 | + or profile_claims.get("email") |
| 182 | + or "" |
| 183 | + ).strip() |
| 184 | + |
| 185 | + token_issued_at = _parse_unix_ts_to_naive_utc(claims.get("iat")) |
| 186 | + token_expires_at = _parse_unix_ts_to_naive_utc(claims.get("exp")) |
| 187 | + extra_data = account.extra_data if isinstance(account.extra_data, dict) else {} |
| 188 | + metadata_expires_at = _parse_iso_to_naive_utc(extra_data.get("expires")) |
| 189 | + |
| 190 | + last_refresh = account.last_refresh or token_issued_at or account.registered_at |
| 191 | + expires_at = account.expires_at or token_expires_at or metadata_expires_at |
| 192 | + |
| 193 | + client_id = str( |
| 194 | + claims.get("client_id") |
| 195 | + or account.client_id |
| 196 | + or "" |
| 197 | + ).strip() |
| 198 | + |
103 | 199 | return { |
104 | 200 | "type": "codex", |
105 | | - "email": account.email, |
106 | | - "expired": account.expires_at.strftime("%Y-%m-%dT%H:%M:%S+08:00") if account.expires_at else "", |
107 | | - "id_token": account.id_token or "", |
108 | | - "account_id": account.account_id or "", |
109 | | - "access_token": account.access_token or "", |
110 | | - "last_refresh": account.last_refresh.strftime("%Y-%m-%dT%H:%M:%S+08:00") if account.last_refresh else "", |
111 | | - "refresh_token": account.refresh_token or "", |
| 201 | + "email": email, |
| 202 | + "expired": _format_cpa_datetime(expires_at), |
| 203 | + "id_token": id_token, |
| 204 | + "account_id": account_id, |
| 205 | + "workspace_id": workspace_id, |
| 206 | + "chatgpt_account_id": account_id, |
| 207 | + "chatgpt_user_id": chatgpt_user_id, |
| 208 | + "client_id": client_id, |
| 209 | + "access_token": access_token, |
| 210 | + "last_refresh": _format_cpa_datetime(last_refresh), |
| 211 | + "refresh_token": refresh_token, |
112 | 212 | } |
113 | 213 |
|
114 | 214 |
|
|
0 commit comments