zai / app /utils /fe_version.py
bg5's picture
Upload 85 files
e0b68d2 verified
Raw History Blame Contribute Delete
4.59 kB
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Utility helpers for resolving the latest X-FE-Version value from chat.z.ai.
The upstream service embeds the current front-end release identifier inside
its landing page static asset URLs (e.g. `prod-fe-1.0.107`). The helpers in
this module fetch the landing page, extract the version string, and cache it
with a configurable TTL so the expensive network fetch only happens when
necessary.
"""
from __future__ import annotations
import asyncio
import re
import time
from typing import Optional
import httpx
from app.core.httpx_errors import normalize_httpx_exception
from app.utils.logger import logger, log_exception
from app.utils.user_agent import get_random_user_agent
# Base URL to probe for the version string.
FE_VERSION_SOURCE_URL = "https://chat.z.ai"
# Cache TTL in seconds (default: 30 minutes).
CACHE_TTL_SECONDS = 1800
_version_pattern = re.compile(r"prod-fe-\d+\.\d+\.\d+")
_cached_version: str = ""
_cached_at: float = 0.0
_version_lock: Optional[asyncio.Lock] = None
_version_lock_loop: Optional[asyncio.AbstractEventLoop] = None
def _extract_version(page_content: str) -> Optional[str]:
"""Extract the version string from the page content."""
if not page_content:
return None
matches = _version_pattern.findall(page_content)
if not matches:
return None
# Choose the highest lexical value to guard against mixed versions.
return max(matches)
def _should_use_cache(force_refresh: bool) -> bool:
"""Determine whether the cached value can be reused."""
if force_refresh:
return False
if not _cached_version:
return False
if _cached_at <= 0:
return False
return (time.time() - _cached_at) < CACHE_TTL_SECONDS
async def get_latest_fe_version(force_refresh: bool = False) -> str:
"""
Resolve the latest X-FE-Version value from chat.z.ai.
The lookup order is:
1. Cached value within TTL.
2. Remote fetch from chat.z.ai.
Raises:
Exception: If unable to fetch the version from the remote source.
"""
global _cached_version, _cached_at, _version_lock, _version_lock_loop
if _should_use_cache(force_refresh):
return _cached_version
current_loop = asyncio.get_running_loop()
if _version_lock is None or _version_lock_loop is not current_loop:
_version_lock = asyncio.Lock()
_version_lock_loop = current_loop
async with _version_lock:
if _should_use_cache(force_refresh):
return _cached_version
try:
headers = {"User-Agent": get_random_user_agent("chrome")}
except Exception:
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
}
try:
async with httpx.AsyncClient(
timeout=10.0,
follow_redirects=True,
trust_env=False,
) as client:
response = await client.get(FE_VERSION_SOURCE_URL, headers=headers)
response.raise_for_status()
version = _extract_version(response.text)
if version:
if version != _cached_version:
logger.debug("[Z.AI] Detected X-FE-Version update: %s", version)
_cached_version = version
_cached_at = time.time()
return version
logger.error("[Z.AI] Unable to locate X-FE-Version in landing page")
raise Exception("Unable to locate X-FE-Version in landing page")
except Exception as exc:
log_exception(
logger,
"%s",
normalize_httpx_exception(
exc,
method="GET",
url=FE_VERSION_SOURCE_URL,
context="fe_version.fetch",
),
append_error=False,
)
if _cached_version:
return _cached_version
raise Exception(f"Failed to fetch X-FE-Version: {exc}")
async def refresh_fe_version() -> str:
"""Force refresh the cached version by bypassing the TTL."""
return await get_latest_fe_version(force_refresh=True)