Merge pull request #135 from shellus/main

feat: 添加浏览器指纹配置支持,优化配置页面分组
This commit is contained in:
Chenyme authored and GitHub committed 2026-02-07 10:46:50 +08:00
commit e0ecde53ce
9 files changed
+156 -91

No files matched your search

+8 -6
View File
@@ -37,7 +37,8 @@ DOWNLOAD_API = "https://assets.grok.com"
LOCK_DIR = Path(__file__).parent.parent.parent.parent / "data" / ".locks"
TIMEOUT = 120
BROWSER = "chrome136"
DEFAULT_BROWSER = "chrome136"
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
DEFAULT_MIME = "application/octet-stream"
# 并发控制
@@ -179,6 +180,7 @@ class BaseService:
def _headers(self, token: str, referer: str = "https://grok.com/") -> dict:
"""构建请求头"""
user_agent = get_config("grok.user_agent", DEFAULT_USER_AGENT)
headers = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br, zstd",
@@ -199,7 +201,7 @@ class BaseService:
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
"User-Agent": user_agent,
}
apply_statsig(headers)
@@ -354,7 +356,7 @@ class UploadService(BaseService):
UPLOAD_API,
headers=headers,
json=payload,
impersonate=BROWSER,
impersonate=get_config("grok.browser", DEFAULT_BROWSER),
timeout=self.timeout,
proxies=self._proxies(),
)
@@ -448,7 +450,7 @@ class ListService(BaseService):
LIST_API,
headers=headers,
params=params,
impersonate=BROWSER,
impersonate=get_config("grok.browser", DEFAULT_BROWSER),
timeout=self.timeout,
proxies=self._proxies(),
)
@@ -527,7 +529,7 @@ class DeleteService(BaseService):
response = await session.delete(
url,
headers=headers,
impersonate=BROWSER,
impersonate=get_config("grok.browser", DEFAULT_BROWSER),
timeout=self.timeout,
proxies=self._proxies(),
)
@@ -663,7 +665,7 @@ class DownloadService(BaseService):
proxies=self._proxies(),
timeout=self.timeout,
allow_redirects=True,
impersonate=BROWSER,
impersonate=get_config("grok.browser", DEFAULT_BROWSER),
stream=True,
)
+6 -3
View File
@@ -26,7 +26,8 @@ from app.services.token import get_token_manager, EffortType
CHAT_API = "https://grok.com/rest/app-chat/conversations/new"
TIMEOUT = 120
BROWSER = "chrome136"
DEFAULT_BROWSER = "chrome136"
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
@dataclass
@@ -162,6 +163,7 @@ class ChatRequestBuilder:
@staticmethod
def build_headers(token: str) -> Dict[str, str]:
"""构造请求头"""
user_agent = get_config("grok.user_agent", DEFAULT_USER_AGENT)
headers = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br, zstd",
@@ -182,7 +184,7 @@ class ChatRequestBuilder:
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
"User-Agent": user_agent,
}
apply_statsig(headers)
@@ -311,7 +313,8 @@ class GrokChatService:
# 建立连接函数
async def establish_connection():
"""建立连接并返回 response 对象"""
session = AsyncSession(impersonate=BROWSER)
browser = get_config("grok.browser", DEFAULT_BROWSER)
session = AsyncSession(impersonate=browser)
try:
response = await session.post(
CHAT_API,
+10 -5
View File
@@ -26,7 +26,8 @@ CREATE_POST_API = "https://grok.com/rest/media/post/create"
CHAT_API = "https://grok.com/rest/app-chat/conversations/new"
# 常量
BROWSER = "chrome136"
DEFAULT_BROWSER = "chrome136"
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
TIMEOUT = 300
DEFAULT_MAX_CONCURRENT = 50
_MEDIA_SEMAPHORE = asyncio.Semaphore(DEFAULT_MAX_CONCURRENT)
@@ -58,6 +59,7 @@ class VideoService:
self, token: str, referer: str = "https://grok.com/imagine"
) -> dict:
"""构建请求头"""
user_agent = get_config("grok.user_agent", DEFAULT_USER_AGENT)
headers = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br, zstd",
@@ -78,7 +80,7 @@ class VideoService:
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
"User-Agent": user_agent,
}
apply_statsig(headers)
@@ -119,11 +121,12 @@ class VideoService:
payload = {"mediaType": media_type, "prompt": prompt}
async with AsyncSession() as session:
browser = get_config("grok.browser", DEFAULT_BROWSER)
response = await session.post(
CREATE_POST_API,
headers=headers,
json=payload,
impersonate=BROWSER,
impersonate=browser,
timeout=30,
proxies=self._build_proxies(),
)
@@ -243,7 +246,8 @@ class VideoService:
prompt, post_id, aspect_ratio, video_length, resolution_name, preset
)
session = AsyncSession(impersonate=BROWSER)
browser = get_config("grok.browser", DEFAULT_BROWSER)
session = AsyncSession(impersonate=browser)
response = await session.post(
CHAT_API,
headers=headers,
@@ -323,7 +327,8 @@ class VideoService:
prompt, post_id, aspect_ratio, video_length, resolution, preset
)
session = AsyncSession(impersonate=BROWSER)
browser = get_config("grok.browser", DEFAULT_BROWSER)
session = AsyncSession(impersonate=browser)
response = await session.post(
CHAT_API,
headers=headers,
+8 -4
View File
@@ -23,7 +23,8 @@ from app.services.grok.utils.headers import build_sso_cookie
NSFW_API = "https://grok.com/auth_mgmt.AuthManagement/UpdateUserFeatureControls"
BIRTH_DATE_API = "https://grok.com/rest/auth/set-birth-date"
BROWSER = "chrome136"
DEFAULT_BROWSER = "chrome136"
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
TIMEOUT = 30
@@ -61,12 +62,13 @@ class NSFWService:
def _build_headers(self, token: str) -> dict:
"""构造 gRPC-Web 请求头"""
cookie = build_sso_cookie(token, include_rw=True)
user_agent = get_config("grok.user_agent", DEFAULT_USER_AGENT)
return {
"accept": "*/*",
"content-type": "application/grpc-web+proto",
"origin": "https://grok.com",
"referer": "https://grok.com/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"user-agent": user_agent,
"x-grpc-web": "1",
"x-user-agent": "connect-es/2.1.1",
"cookie": cookie,
@@ -75,12 +77,13 @@ class NSFWService:
def _build_birth_headers(self, token: str) -> dict:
"""构造设置出生日期请求头"""
cookie = build_sso_cookie(token, include_rw=True)
user_agent = get_config("grok.user_agent", DEFAULT_USER_AGENT)
return {
"accept": "*/*",
"content-type": "application/json",
"origin": "https://grok.com",
"referer": "https://grok.com/?_s=account",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"user-agent": user_agent,
"cookie": cookie,
}
@@ -127,7 +130,8 @@ class NSFWService:
proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None
try:
async with AsyncSession(impersonate=BROWSER) as session:
browser = get_config("grok.browser", DEFAULT_BROWSER)
async with AsyncSession(impersonate=browser) as session:
ok, birth_status, birth_err = await self._set_birth_date(
session, token
)
+6 -3
View File
@@ -15,7 +15,8 @@ from app.services.grok.utils.retry import retry_on_status
LIMITS_API = "https://grok.com/rest/rate-limits"
BROWSER = "chrome136"
DEFAULT_BROWSER = "chrome136"
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
TIMEOUT = 10
DEFAULT_MAX_CONCURRENT = 25
_USAGE_SEMAPHORE = asyncio.Semaphore(DEFAULT_MAX_CONCURRENT)
@@ -45,6 +46,7 @@ class UsageService:
def _build_headers(self, token: str) -> dict:
"""构建请求头"""
user_agent = get_config("grok.user_agent", DEFAULT_USER_AGENT)
headers = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br, zstd",
@@ -65,7 +67,7 @@ class UsageService:
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
"User-Agent": user_agent,
}
apply_statsig(headers)
@@ -103,13 +105,14 @@ class UsageService:
try:
headers = self._build_headers(token)
payload = {"requestKind": "DEFAULT", "modelName": model_name}
browser = get_config("grok.browser", DEFAULT_BROWSER)
async with AsyncSession() as session:
response = await session.post(
LIMITS_API,
headers=headers,
json=payload,
impersonate=BROWSER,
impersonate=browser,
timeout=self.timeout,
proxies=self._build_proxies(),
)
+3 -2
View File
@@ -15,7 +15,7 @@ from app.services.grok.utils.headers import apply_statsig, build_sso_cookie
LIVEKIT_TOKEN_API = "https://grok.com/rest/livekit/tokens"
TIMEOUT = 30
BROWSER = "chrome136"
DEFAULT_BROWSER = "chrome136"
class VoiceService:
@@ -45,7 +45,8 @@ class VoiceService:
proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None
try:
async with AsyncSession(impersonate=BROWSER) as session:
browser = get_config("grok.browser", DEFAULT_BROWSER)
async with AsyncSession(impersonate=browser) as session:
response = await session.post(
LIVEKIT_TOKEN_API,
headers=headers,
+1 -1
View File
@@ -50,7 +50,7 @@
<script src="/static/common/header.js?v=3"></script>
<script src="/static/common/footer.js?v=1"></script>
<script src="/static/common/toast.js"></script>
<script src="/static/config/config.js?v=7"></script>
<script src="/static/config/config.js?v=13"></script>
</body>
</html>
+110 -67
View File
@@ -41,6 +41,7 @@ const LOCALE_MAP = {
"grok": {
"label": "Grok 设置",
"temporary": { title: "临时对话", desc: "是否启用临时对话模式。" },
"disable_memory": { title: "禁用记忆", desc: "禁用 Grok 记忆功能,以防止响应中出现不相关上下文" },
"stream": { title: "流式响应", desc: "是否默认启用流式输出。" },
"thinking": { title: "思维链", desc: "是否启用模型思维链输出。" },
"dynamic_statsig": { title: "动态指纹", desc: "是否启用动态生成 Statsig 值。" },
@@ -48,7 +49,6 @@ const LOCALE_MAP = {
"timeout": { title: "超时时间", desc: "请求 Grok 服务的超时时间(秒)。" },
"base_proxy_url": { title: "基础代理 URL", desc: "代理请求到 Grok 官网的基础服务地址。" },
"asset_proxy_url": { title: "资源代理 URL", desc: "代理请求到 Grok 官网的静态资源(图片/视频)地址。" },
"cf_clearance": { title: "CF Clearance", desc: "Cloudflare 验证 Cookie,用于验证 Cloudflare 的验证。" },
"max_retry": { title: "最大重试", desc: "请求 Grok 服务失败时的最大重试次数。" },
"retry_status_codes": { title: "重试状态码", desc: "触发重试的 HTTP 状态码列表。" },
"retry_backoff_base": { title: "退避基数", desc: "重试退避的基础延迟(秒)。" },
@@ -56,7 +56,10 @@ const LOCALE_MAP = {
"retry_backoff_max": { title: "退避上限", desc: "单次重试等待的最大延迟(秒)。" },
"retry_budget": { title: "退避预算", desc: "单次请求的最大重试总耗时(秒)。" },
"stream_idle_timeout": { title: "流空闲超时", desc: "流式响应空闲超时(秒),超过将断开。" },
"video_idle_timeout": { title: "视频空闲超时", desc: "视频生成空闲超时(秒),超过将断开。" }
"video_idle_timeout": { title: "视频空闲超时", desc: "视频生成空闲超时(秒),超过将断开。" },
"cf_clearance": { title: "CF Clearance", desc: "" },
"browser": { title: "curl_cffi", desc: "" },
"user_agent": { title: "User-Agent", desc: "" }
},
"token": {
"label": "Token 池设置",
@@ -88,6 +91,13 @@ const LOCALE_MAP = {
}
};
// 反爬虫验证字段(从 grok 中单独分组显示)
const ANTIBOT_FIELDS = ['cf_clearance', 'browser', 'user_agent'];
const ANTIBOT_GROUP = {
label: "反爬虫验证",
desc: "以下三个参数用于绕过 Cloudflare 反爬虫验证。配置不正确将导致 403 错误。服务首次请求 Grok 时的 IP 必须与获取 CF Clearance 时的 IP 一致,后续服务器请求 IP 变化不会导致403。"
};
const SECTION_ORDER = new Map(Object.keys(LOCALE_MAP).map((key, index) => [key, index]));
function getText(section, key) {
@@ -234,80 +244,53 @@ function renderConfig(data) {
const localeSection = LOCALE_MAP[section];
const keyOrder = localeSection ? new Map(Object.keys(localeSection).map((k, i) => [k, i])) : null;
const card = document.createElement('div');
card.className = 'config-section';
// 分离反爬虫字段和普通字段
const allKeys = sortByOrder(Object.keys(items), keyOrder);
const normalKeys = section === 'grok' ? allKeys.filter(k => !ANTIBOT_FIELDS.includes(k)) : allKeys;
const antibotKeys = section === 'grok' ? allKeys.filter(k => ANTIBOT_FIELDS.includes(k)) : [];
const header = document.createElement('div');
header.innerHTML = `<div class="config-section-title">${getSectionLabel(section)}</div>`;
card.appendChild(header);
// 渲染普通字段
if (normalKeys.length > 0) {
const card = document.createElement('div');
card.className = 'config-section';
const grid = document.createElement('div');
grid.className = 'config-grid';
const header = document.createElement('div');
header.innerHTML = `<div class="config-section-title">${getSectionLabel(section)}</div>`;
card.appendChild(header);
const keys = sortByOrder(Object.keys(items), keyOrder);
const grid = document.createElement('div');
grid.className = 'config-grid';
keys.forEach(key => {
const val = items[key];
const text = getText(section, key);
normalKeys.forEach(key => {
const fieldCard = buildFieldCard(section, key, items[key]);
grid.appendChild(fieldCard);
});
// Container
const fieldCard = document.createElement('div');
fieldCard.className = 'config-field';
// Title
const titleEl = document.createElement('div');
titleEl.className = 'config-field-title';
titleEl.textContent = text.title;
fieldCard.appendChild(titleEl);
// Description (Muted)
const descEl = document.createElement('p');
descEl.className = 'config-field-desc';
descEl.textContent = text.desc;
fieldCard.appendChild(descEl);
// Input Wrapper
const inputWrapper = document.createElement('div');
inputWrapper.className = 'config-field-input';
// Input Logic
let built;
if (typeof val === 'boolean') {
built = buildBooleanInput(section, key, val);
}
else if (key === 'image_format') {
built = buildSelectInput(section, key, val, [
{ val: 'url', text: 'URL' },
{ val: 'base64', text: 'Base64' }
]);
}
else if (key === 'video_format') {
built = buildSelectInput(section, key, val, [
{ val: 'html', text: 'HTML' },
{ val: 'url', text: 'URL' }
]);
}
else if (Array.isArray(val) || typeof val === 'object') {
built = buildJsonInput(section, key, val);
}
else {
if (key === 'api_key' || key === 'app_key') {
built = buildSecretInput(section, key, val);
} else {
built = buildTextInput(section, key, val);
}
card.appendChild(grid);
if (grid.children.length > 0) {
fragment.appendChild(card);
}
}
if (built) {
inputWrapper.appendChild(built.node);
}
fieldCard.appendChild(inputWrapper);
grid.appendChild(fieldCard);
});
// 渲染反爬虫验证分组(仅 grok section)
if (antibotKeys.length > 0) {
const card = document.createElement('div');
card.className = 'config-section';
card.appendChild(grid);
const header = document.createElement('div');
header.innerHTML = `<div class="config-section-title">${ANTIBOT_GROUP.label}</div>
<p class="text-[var(--accents-4)] text-sm mt-1 mb-4">${ANTIBOT_GROUP.desc}</p>`;
card.appendChild(header);
if (grid.children.length > 0) {
const grid = document.createElement('div');
grid.className = 'config-grid';
antibotKeys.forEach(key => {
const fieldCard = buildFieldCard(section, key, items[key]);
grid.appendChild(fieldCard);
});
card.appendChild(grid);
fragment.appendChild(card);
}
});
@@ -315,6 +298,66 @@ function renderConfig(data) {
container.appendChild(fragment);
}
function buildFieldCard(section, key, val) {
const text = getText(section, key);
const fieldCard = document.createElement('div');
fieldCard.className = 'config-field';
// Title
const titleEl = document.createElement('div');
titleEl.className = 'config-field-title';
titleEl.textContent = text.title;
fieldCard.appendChild(titleEl);
// Description (Muted) - 只在有描述时显示
if (text.desc) {
const descEl = document.createElement('p');
descEl.className = 'config-field-desc';
descEl.textContent = text.desc;
fieldCard.appendChild(descEl);
}
// Input Wrapper
const inputWrapper = document.createElement('div');
inputWrapper.className = 'config-field-input';
// Input Logic
let built;
if (typeof val === 'boolean') {
built = buildBooleanInput(section, key, val);
}
else if (key === 'image_format') {
built = buildSelectInput(section, key, val, [
{ val: 'url', text: 'URL' },
{ val: 'base64', text: 'Base64' }
]);
}
else if (key === 'video_format') {
built = buildSelectInput(section, key, val, [
{ val: 'html', text: 'HTML' },
{ val: 'url', text: 'URL' }
]);
}
else if (Array.isArray(val) || typeof val === 'object') {
built = buildJsonInput(section, key, val);
}
else {
if (key === 'api_key' || key === 'app_key') {
built = buildSecretInput(section, key, val);
} else {
built = buildTextInput(section, key, val);
}
}
if (built) {
inputWrapper.appendChild(built.node);
}
fieldCard.appendChild(inputWrapper);
return fieldCard;
}
async function saveConfig() {
const btn = byId('save-btn');
const originalText = btn.innerText;
+4
View File
@@ -8,6 +8,10 @@ timeout = 120
base_proxy_url = ""
asset_proxy_url = ""
cf_clearance = ""
# 浏览器指纹配置
browser = "chrome136"
user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
disable_memory = true
max_retry = 3
retry_status_codes = [401,429,403]
# 重试退避配置