Skip to content

Commit 8d4ab22

Browse files
committed
✨ feat(zai): implement dynamic FE version management
- 添加动态获取 X-FE-Version 的功能 - 新增 fe_version.py 模块用于版本管理 - 实现版本缓存机制,减少网络请求 - 更新 ZAIProvider 使用动态版本号 - 移除硬编码的版本号 - 优化日志输出,移除冗余信息
1 parent 4644168 commit 8d4ab22

3 files changed

Lines changed: 118 additions & 4 deletions

File tree

app/providers/zai_provider.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from datetime import datetime
2121
from typing import Dict, List, Any, Optional, AsyncGenerator, Union
2222
from app.utils.user_agent import get_random_user_agent
23+
from app.utils.fe_version import get_latest_fe_version
2324
from app.utils.signature import generate_signature
2425
from app.providers.base import BaseProvider, ProviderConfig
2526
from app.models.schemas import OpenAIRequest, Message
@@ -42,6 +43,7 @@ def get_zai_dynamic_headers(chat_id: str = "") -> Dict[str, str]:
4243
browser_choices = ["chrome", "chrome", "chrome", "edge", "edge", "firefox", "safari"]
4344
browser_type = random.choice(browser_choices)
4445
user_agent = get_random_user_agent(browser_type)
46+
fe_version = get_latest_fe_version()
4547

4648
chrome_version = "139"
4749
edge_version = "139"
@@ -70,7 +72,7 @@ def get_zai_dynamic_headers(chat_id: str = "") -> Dict[str, str]:
7072
"Cache-Control": "no-cache",
7173
"User-Agent": user_agent,
7274
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
73-
"X-FE-Version": "prod-fe-1.0.106",
75+
"X-FE-Version": fe_version,
7476
"Origin": "https://chat.z.ai",
7577
}
7678

@@ -636,6 +638,7 @@ async def transform_request(self, request: OpenAIRequest) -> Dict[str, Any]:
636638
user_id = _extract_user_id_from_token(token)
637639
timestamp_ms = int(time.time() * 1000)
638640
request_id = generate_uuid()
641+
fe_version = get_latest_fe_version()
639642
try:
640643
signing_metadata = f"requestId,{request_id},timestamp,{timestamp_ms},user_id,{user_id}"
641644
prompt_for_signature = last_user_text or ""
@@ -650,11 +653,11 @@ async def transform_request(self, request: OpenAIRequest) -> Dict[str, Any]:
650653
logger.error(f"[Z.AI] 签名生成失败: {e}")
651654
signature = ""
652655

653-
# 构建请求头 (匹配 X-FE-Version 和 X-Signature)
656+
# 构建请求头
654657
headers = {
655658
"Authorization": f"Bearer {token}",
656659
"Content-Type": "application/json",
657-
"X-FE-Version": "prod-fe-1.0.106",
660+
"X-FE-Version": fe_version,
658661
"X-Signature": signature,
659662
}
660663

app/utils/fe_version.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
Utility helpers for resolving the latest X-FE-Version value from chat.z.ai.
6+
7+
The upstream service embeds the current front-end release identifier inside
8+
its landing page static asset URLs (e.g. `prod-fe-1.0.107`). The helpers in
9+
this module fetch the landing page, extract the version string, and cache it
10+
with a configurable TTL so the expensive network fetch only happens when
11+
necessary.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import re
17+
import time
18+
from typing import Optional
19+
20+
import httpx
21+
22+
from app.utils.logger import get_logger
23+
from app.utils.user_agent import get_random_user_agent
24+
25+
# Base URL to probe for the version string.
26+
FE_VERSION_SOURCE_URL = "https://chat.z.ai"
27+
28+
# Cache TTL in seconds (default: 30 minutes).
29+
CACHE_TTL_SECONDS = 1800
30+
31+
_logger = get_logger()
32+
_version_pattern = re.compile(r"prod-fe-\d+\.\d+\.\d+")
33+
34+
_cached_version: str = ""
35+
_cached_at: float = 0.0
36+
37+
38+
def _extract_version(page_content: str) -> Optional[str]:
39+
"""Extract the version string from the page content."""
40+
if not page_content:
41+
return None
42+
43+
matches = _version_pattern.findall(page_content)
44+
if not matches:
45+
return None
46+
47+
# Choose the highest lexical value to guard against mixed versions.
48+
return max(matches)
49+
50+
51+
52+
53+
def _should_use_cache(force_refresh: bool) -> bool:
54+
"""Determine whether the cached value can be reused."""
55+
if force_refresh:
56+
return False
57+
if not _cached_version:
58+
return False
59+
if _cached_at <= 0:
60+
return False
61+
return (time.time() - _cached_at) < CACHE_TTL_SECONDS
62+
63+
64+
def get_latest_fe_version(force_refresh: bool = False) -> str:
65+
"""
66+
Resolve the latest X-FE-Version value from chat.z.ai.
67+
68+
The lookup order is:
69+
1. Cached value within TTL.
70+
2. Remote fetch from chat.z.ai.
71+
72+
Raises:
73+
Exception: If unable to fetch the version from the remote source.
74+
"""
75+
global _cached_version, _cached_at
76+
77+
if _should_use_cache(force_refresh):
78+
return _cached_version
79+
80+
try:
81+
headers = {"User-Agent": get_random_user_agent("chrome")}
82+
except Exception:
83+
headers = {
84+
"User-Agent": (
85+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
86+
"AppleWebKit/537.36 (KHTML, like Gecko) "
87+
"Chrome/120.0.0.0 Safari/537.36"
88+
)
89+
}
90+
91+
try:
92+
with httpx.Client(timeout=10.0, follow_redirects=True) as client:
93+
response = client.get(FE_VERSION_SOURCE_URL, headers=headers)
94+
response.raise_for_status()
95+
version = _extract_version(response.text)
96+
if version:
97+
if version != _cached_version:
98+
_logger.info(f"[Z.AI] Detected X-FE-Version update: {version}")
99+
_cached_version = version
100+
_cached_at = time.time()
101+
return version
102+
103+
_logger.error("[Z.AI] Unable to locate X-FE-Version in landing page")
104+
raise Exception("Unable to locate X-FE-Version in landing page")
105+
except Exception as exc:
106+
_logger.error(f"[Z.AI] Failed to fetch X-FE-Version from {FE_VERSION_SOURCE_URL}: {exc}")
107+
raise Exception(f"Failed to fetch X-FE-Version: {exc}")
108+
109+
110+
def refresh_fe_version() -> str:
111+
"""Force refresh the cached version by bypassing the TTL."""
112+
return get_latest_fe_version(force_refresh=True)

app/utils/logger.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ def setup_logger(log_dir, log_retention_days=7, log_rotation="1 day", debug_mode
5656
enqueue=True,
5757
catch=True,
5858
)
59-
logger.info(f"✅ 日志文件输出已启用: {log_dir}")
6059
except (PermissionError, OSError) as e:
6160
# 如果无法创建日志目录或文件,降级为仅控制台输出
6261
logger.warning(f"⚠️ 无法创建日志文件 ({e}),将仅使用控制台输出")

0 commit comments

Comments
 (0)