diff --git a/app/services/grok/services/assets.py b/app/services/grok/services/assets.py index 59f041b..15791f4 100644 --- a/app/services/grok/services/assets.py +++ b/app/services/grok/services/assets.py @@ -37,7 +37,8 @@ DOWNLOAD_API = "https://assets.grok.com" LOCK_DIR = Path(__file__).parent.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, ) diff --git a/app/services/grok/services/chat.py b/app/services/grok/services/chat.py index 1609f89..237cd38 100644 --- a/app/services/grok/services/chat.py +++ b/app/services/grok/services/chat.py @@ -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) @@ -321,7 +323,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, diff --git a/app/services/grok/services/media.py b/app/services/grok/services/media.py index da4f1af..7b0eab7 100644 --- a/app/services/grok/services/media.py +++ b/app/services/grok/services/media.py @@ -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, diff --git a/app/services/grok/services/nsfw.py b/app/services/grok/services/nsfw.py index 2041ec1..be797a4 100644 --- a/app/services/grok/services/nsfw.py +++ b/app/services/grok/services/nsfw.py @@ -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 ) diff --git a/app/services/grok/services/usage.py b/app/services/grok/services/usage.py index e6b7c57..a339962 100644 --- a/app/services/grok/services/usage.py +++ b/app/services/grok/services/usage.py @@ -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(), ) diff --git a/app/services/grok/services/voice.py b/app/services/grok/services/voice.py index 9e03880..8b87991 100644 --- a/app/services/grok/services/voice.py +++ b/app/services/grok/services/voice.py @@ -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, diff --git a/app/static/config/config.html b/app/static/config/config.html index 6d70398..7fb207f 100644 --- a/app/static/config/config.html +++ b/app/static/config/config.html @@ -50,7 +50,7 @@ - + diff --git a/app/static/config/config.js b/app/static/config/config.js index b94d047..bcb8036 100644 --- a/app/static/config/config.js +++ b/app/static/config/config.js @@ -43,6 +43,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 值。" }, @@ -50,7 +51,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 的验证。" }, "image_ws": { title: "Imagine WebSocket 生成", desc: "启用后 /v1/images/generations 走 WebSocket 直连。" }, "image_ws_nsfw": { title: "Imagine NSFW", desc: "WS 请求里是否启用 NSFW。" }, "image_ws_blocked_seconds": { title: "Imagine WebSocket Blocked 阈值", desc: "收到中等图后超过该秒数仍无最终图则判定 blocked。" }, @@ -62,7 +62,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 池设置", @@ -94,6 +97,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) { @@ -240,80 +250,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 = `
${getSectionLabel(section)}
`; - 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 = `
${getSectionLabel(section)}
`; + 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 = `
${ANTIBOT_GROUP.label}
+

${ANTIBOT_GROUP.desc}

`; + 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); } }); @@ -321,6 +304,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; diff --git a/config.defaults.toml b/config.defaults.toml index 131996e..d425cb1 100644 --- a/config.defaults.toml +++ b/config.defaults.toml @@ -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] image_ws = true