|
46 | 46 |
|
47 | 47 | # Cookie-based authentication endpoint |
48 | 48 | IFLOW_API_KEY_ENDPOINT = "https://platform.iflow.cn/api/openapi/apikey" |
| 49 | +IFLOW_DEFAULT_API_BASE = "https://apis.iflow.cn/v1" |
49 | 50 |
|
50 | 51 | # Client credentials provided by iFlow |
51 | 52 | IFLOW_CLIENT_ID = "10009311001" |
@@ -335,6 +336,32 @@ def __init__(self): |
335 | 336 | self._refresh_interval_seconds: int = 30 # Delay between queue items |
336 | 337 | self._refresh_max_retries: int = 3 # Attempts before kicked out |
337 | 338 |
|
| 339 | + def get_api_base_candidates(self) -> List[str]: |
| 340 | + """Return ordered iFlow API base candidates from environment variables.""" |
| 341 | + candidates: List[str] = [] |
| 342 | + |
| 343 | + single_base = os.getenv("IFLOW_API_BASE", "").strip() |
| 344 | + if single_base: |
| 345 | + normalized = single_base.rstrip("/") |
| 346 | + if normalized: |
| 347 | + candidates.append(normalized) |
| 348 | + |
| 349 | + base_list = os.getenv("IFLOW_API_BASES", "").strip() |
| 350 | + if base_list: |
| 351 | + for item in base_list.split(","): |
| 352 | + normalized = item.strip().rstrip("/") |
| 353 | + if normalized and normalized not in candidates: |
| 354 | + candidates.append(normalized) |
| 355 | + |
| 356 | + if IFLOW_DEFAULT_API_BASE not in candidates: |
| 357 | + candidates.append(IFLOW_DEFAULT_API_BASE) |
| 358 | + |
| 359 | + return candidates |
| 360 | + |
| 361 | + def get_api_base(self) -> str: |
| 362 | + """Return the primary iFlow API base URL.""" |
| 363 | + return self.get_api_base_candidates()[0] |
| 364 | + |
338 | 365 | def _parse_env_credential_path(self, path: str) -> Optional[str]: |
339 | 366 | """ |
340 | 367 | Parse a virtual env:// path and return the credential index. |
@@ -978,12 +1005,32 @@ async def _refresh_token(self, path: str, force: bool = False) -> Dict[str, Any] |
978 | 1005 | if not force and cached_creds and not self._is_token_expired(cached_creds): |
979 | 1006 | return cached_creds |
980 | 1007 |
|
981 | | - # [ROTATING TOKEN FIX] Always read fresh from disk before refresh. |
982 | | - # iFlow may use rotating refresh tokens - each refresh could invalidate the previous token. |
983 | | - # If we use a stale cached token, refresh will fail. |
984 | | - # Reading fresh from disk ensures we have the latest token. |
985 | | - await self._read_creds_from_file(path) |
986 | | - creds_from_file = self._credentials_cache[path] |
| 1008 | + # For file-based credentials, read fresh from disk before refresh. |
| 1009 | + # For env-loaded credentials, refresh using cached/env values (no file IO). |
| 1010 | + creds_from_file: Optional[Dict[str, Any]] = None |
| 1011 | + if cached_creds and cached_creds.get("_proxy_metadata", {}).get( |
| 1012 | + "loaded_from_env" |
| 1013 | + ): |
| 1014 | + creds_from_file = cached_creds |
| 1015 | + elif self._parse_env_credential_path(path) is not None: |
| 1016 | + credential_index = self._parse_env_credential_path(path) |
| 1017 | + env_creds = self._load_from_env(credential_index) |
| 1018 | + if env_creds: |
| 1019 | + self._credentials_cache[path] = env_creds |
| 1020 | + creds_from_file = env_creds |
| 1021 | + else: |
| 1022 | + raise ValueError( |
| 1023 | + f"No environment credentials found for iFlow path: {path}" |
| 1024 | + ) |
| 1025 | + else: |
| 1026 | + # [ROTATING TOKEN FIX] Always read fresh from disk before refresh. |
| 1027 | + # iFlow may use rotating refresh tokens - each refresh could invalidate |
| 1028 | + # the previous token. Fresh disk read keeps us in sync. |
| 1029 | + await self._read_creds_from_file(path) |
| 1030 | + creds_from_file = self._credentials_cache[path] |
| 1031 | + |
| 1032 | + if creds_from_file is None: |
| 1033 | + raise ValueError(f"No credentials available for iFlow refresh: {path}") |
987 | 1034 |
|
988 | 1035 | lib_logger.debug(f"Refreshing iFlow OAuth token for '{Path(path).name}'...") |
989 | 1036 | refresh_token = creds_from_file.get("refresh_token") |
@@ -1228,7 +1275,8 @@ async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]: |
1228 | 1275 | - API Key: credential_identifier is the API key string itself |
1229 | 1276 | """ |
1230 | 1277 | # Detect credential type |
1231 | | - if os.path.isfile(credential_identifier): |
| 1278 | + credential_index = self._parse_env_credential_path(credential_identifier) |
| 1279 | + if credential_index is not None or os.path.isfile(credential_identifier): |
1232 | 1280 | creds = await self._load_credentials(credential_identifier) |
1233 | 1281 |
|
1234 | 1282 | # Check if this is a cookie-based credential |
@@ -1262,7 +1310,7 @@ async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]: |
1262 | 1310 | lib_logger.debug("Using direct API key for iFlow") |
1263 | 1311 | api_key = credential_identifier |
1264 | 1312 |
|
1265 | | - base_url = "https://apis.iflow.cn/v1" |
| 1313 | + base_url = self.get_api_base() |
1266 | 1314 | return base_url, api_key |
1267 | 1315 |
|
1268 | 1316 | async def proactively_refresh(self, credential_identifier: str): |
|
0 commit comments