From bcd94069a74b6264b2cb7f4744c611b14beb1c48 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:30:50 +0800 Subject: [PATCH] feat: migrate configuration structure to improve organization and flexibility --- app/api/v1/image.py | 10 +- app/core/config.py | 87 ++++++++++- app/services/grok/defaults.py | 76 ++++++---- .../grok/processors/chat_processors.py | 10 +- .../grok/processors/image_processors.py | 4 +- .../grok/processors/video_processors.py | 6 +- app/services/grok/services/assets.py | 8 +- app/services/grok/services/chat.py | 18 +-- app/services/grok/services/image.py | 12 +- app/services/grok/services/media.py | 12 +- app/services/grok/services/nsfw.py | 14 +- app/services/grok/services/usage.py | 8 +- app/services/grok/services/voice.py | 6 +- app/services/grok/utils/headers.py | 2 +- app/services/grok/utils/retry.py | 12 +- app/services/grok/utils/statsig.py | 2 +- app/static/config/config.html | 2 +- app/static/config/config.js | 106 +++++++------ config.defaults.toml | 139 ++++++++++++++---- data/config.toml | 93 ++++++++---- docs/README.en.md | 54 ++++--- readme.md | 60 ++++---- 22 files changed, 488 insertions(+), 253 deletions(-) diff --git a/app/api/v1/image.py b/app/api/v1/image.py index 9a382e2..fc588ff 100644 --- a/app/api/v1/image.py +++ b/app/api/v1/image.py @@ -93,7 +93,7 @@ def _validate_common_request( # WS 流式仅支持 b64_json (base64 视为同义) if ( request.stream - and get_config("grok.image_ws") + and get_config("image.image_ws") and request.response_format and request.response_format not in {"b64_json", "base64"} ): @@ -319,13 +319,13 @@ async def create_image(request: ImageGenerationRequest): # 获取 token 和模型信息 token_mgr, token = await _get_token(request.model) model_info = ModelService.get(request.model) - use_ws = bool(get_config("grok.image_ws")) + use_ws = bool(get_config("image.image_ws")) # 流式模式 if request.stream: if use_ws: aspect_ratio = resolve_aspect_ratio(request.size) - enable_nsfw = bool(get_config("grok.image_ws_nsfw")) + enable_nsfw = bool(get_config("image.image_ws_nsfw")) upstream = image_service.stream( token=token, prompt=request.prompt, @@ -372,7 +372,7 @@ async def create_image(request: ImageGenerationRequest): usage_override = None if use_ws: aspect_ratio = resolve_aspect_ratio(request.size) - enable_nsfw = bool(get_config("grok.image_ws_nsfw")) + enable_nsfw = bool(get_config("image.image_ws_nsfw")) all_images = [] seen = set() expected_per_call = 6 @@ -643,7 +643,7 @@ async def edit_image( ] = parent_post_id raw_payload = { - "temporary": bool(get_config("grok.temporary")), + "temporary": bool(get_config("chat.temporary")), "modelName": model_info.grok_model, "message": edit_request.prompt, "enableImageGeneration": True, diff --git a/app/core/config.py b/app/core/config.py index d9d3e8c..22c125b 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -32,6 +32,81 @@ def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any return result +def _migrate_deprecated_config(config: Dict[str, Any], valid_sections: set) -> tuple[Dict[str, Any], set]: + """ + 迁移废弃的配置节到新配置结构 + + Returns: + (迁移后的配置, 废弃的配置节集合) + """ + # 配置映射规则:旧配置 -> 新配置 + MIGRATION_MAP = { + # grok.* -> 对应的新配置节 + "grok.temporary": "chat.temporary", + "grok.disable_memory": "chat.disable_memory", + "grok.stream": "chat.stream", + "grok.thinking": "chat.thinking", + "grok.dynamic_statsig": "chat.dynamic_statsig", + "grok.filter_tags": "chat.filter_tags", + + "grok.timeout": "network.timeout", + "grok.base_proxy_url": "network.base_proxy_url", + "grok.asset_proxy_url": "network.asset_proxy_url", + + "grok.cf_clearance": "security.cf_clearance", + "grok.browser": "security.browser", + "grok.user_agent": "security.user_agent", + + "grok.max_retry": "retry.max_retry", + "grok.retry_status_codes": "retry.retry_status_codes", + "grok.retry_backoff_base": "retry.retry_backoff_base", + "grok.retry_backoff_factor": "retry.retry_backoff_factor", + "grok.retry_backoff_max": "retry.retry_backoff_max", + "grok.retry_budget": "retry.retry_budget", + + "grok.stream_idle_timeout": "timeout.stream_idle_timeout", + "grok.video_idle_timeout": "timeout.video_idle_timeout", + + "grok.image_ws": "image.image_ws", + "grok.image_ws_nsfw": "image.image_ws_nsfw", + "grok.image_ws_blocked_seconds": "image.image_ws_blocked_seconds", + "grok.image_ws_final_min_bytes": "image.image_ws_final_min_bytes", + "grok.image_ws_medium_min_bytes": "image.image_ws_medium_min_bytes", + } + + deprecated_sections = set(config.keys()) - valid_sections + if not deprecated_sections: + return config, set() + + result = {k: deepcopy(v) for k, v in config.items() if k in valid_sections} + migrated_count = 0 + + # 处理废弃配置节中的配置项 + for old_section in deprecated_sections: + if old_section not in config or not isinstance(config[old_section], dict): + continue + + for old_key, old_value in config[old_section].items(): + # 查找映射规则 + old_path = f"{old_section}.{old_key}" + new_path = MIGRATION_MAP.get(old_path) + + if new_path: + new_section, new_key = new_path.split(".", 1) + # 确保新配置节存在 + if new_section not in result: + result[new_section] = {} + # 迁移配置项(保留用户的自定义值) + result[new_section][new_key] = old_value + migrated_count += 1 + logger.debug(f"Migrated config: {old_path} -> {new_path} = {old_value}") + + if migrated_count > 0: + logger.info(f"Migrated {migrated_count} config items from deprecated sections") + + return result, deprecated_sections + + def _load_defaults() -> Dict[str, Any]: """加载默认配置文件""" if not DEFAULT_CONFIG_FILE.exists(): @@ -91,10 +166,18 @@ class Config: config_data = {} config_data = config_data or {} + + # 检查是否有废弃的配置节 + valid_sections = set(self._defaults.keys()) + config_data, deprecated_sections = _migrate_deprecated_config(config_data, valid_sections) + if deprecated_sections: + logger.info(f"Cleaned deprecated config sections: {deprecated_sections}") + merged = _deep_merge(self._defaults, config_data) # 自动回填缺失配置到存储 - should_persist = (not from_remote) or (merged != config_data) + # 或迁移了配置后需要更新 + should_persist = (not from_remote) or (merged != config_data) or deprecated_sections if should_persist: async with storage.acquire_lock("config_save", timeout=10): await storage.save_config(merged) @@ -102,6 +185,8 @@ class Config: logger.info( f"Initialized remote storage ({storage.__class__.__name__}) with config baseline." ) + if deprecated_sections: + logger.info("Configuration automatically migrated and cleaned.") self._config = merged except Exception as e: diff --git a/app/services/grok/defaults.py b/app/services/grok/defaults.py index 78ade9a..e23f515 100644 --- a/app/services/grok/defaults.py +++ b/app/services/grok/defaults.py @@ -6,60 +6,84 @@ Grok 服务默认配置 # Grok 服务默认配置 GROK_DEFAULTS = { - "grok": { - # 网络配置 - "browser": "chrome136", + "app": { + "app_url": "", + "app_key": "grok2api", + "api_key": "", + "image_format": "url", + "video_format": "html", + }, + + "network": { "timeout": 120, - "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", "base_proxy_url": "", "asset_proxy_url": "", + }, + + "security": { "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", + }, + + "chat": { "temporary": True, "disable_memory": True, "stream": True, "thinking": False, + "dynamic_statsig": True, "filter_tags": ["grok:render", "xaiartifact", "xai:tool_usage_card"], - - # 重试配置 + }, + + "retry": { "max_retry": 3, "retry_status_codes": [401, 429, 403], "retry_backoff_base": 0.5, "retry_backoff_factor": 2.0, "retry_backoff_max": 30.0, "retry_budget": 90.0, - - # 超时配置 + }, + + "timeout": { "stream_idle_timeout": 45.0, "video_idle_timeout": 90.0, - - # 图片配置 + }, + + "image": { + "image_ws": True, + "image_ws_nsfw": True, "image_ws_blocked_seconds": 15, "image_ws_final_min_bytes": 100000, "image_ws_medium_min_bytes": 30000, - - # Statsig - "dynamic_statsig": True, }, - "app": { - "app_url": "", - "image_format": "url", - "video_format": "html", - }, - - "performance": { - "assets_max_concurrent": 25, - "assets_delete_batch_size": 10, - "media_max_concurrent": 50, - "usage_max_concurrent": 25, + "token": { + "auto_refresh": True, + "refresh_interval_hours": 8, + "super_refresh_interval_hours": 2, + "fail_threshold": 5, + "save_delay_ms": 500, + "reload_interval_sec": 30, }, "cache": { "enable_auto_clean": True, "limit_mb": 1024, }, + + "performance": { + "assets_max_concurrent": 25, + "assets_delete_batch_size": 10, + "assets_batch_size": 10, + "assets_max_tokens": 1000, + "media_max_concurrent": 50, + "usage_max_concurrent": 25, + "usage_batch_size": 50, + "usage_max_tokens": 1000, + "nsfw_max_concurrent": 10, + "nsfw_batch_size": 50, + "nsfw_max_tokens": 1000, + }, } diff --git a/app/services/grok/processors/chat_processors.py b/app/services/grok/processors/chat_processors.py index a7bf5d4..d7002ca 100644 --- a/app/services/grok/processors/chat_processors.py +++ b/app/services/grok/processors/chat_processors.py @@ -32,13 +32,13 @@ class StreamProcessor(BaseProcessor): self.fingerprint: str = "" self.think_opened: bool = False self.role_sent: bool = False - self.filter_tags = get_config("grok.filter_tags") + self.filter_tags = get_config("chat.filter_tags") self.image_format = get_config("app.image_format") self._tag_buffer: str = "" self._in_filter_tag: bool = False if think is None: - self.show_think = get_config("grok.thinking") + self.show_think = get_config("chat.thinking") else: self.show_think = think @@ -114,7 +114,7 @@ class StreamProcessor(BaseProcessor): async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, None]: """处理流式响应""" - idle_timeout = get_config("grok.stream_idle_timeout") + idle_timeout = get_config("timeout.stream_idle_timeout") try: async for line in _with_idle_timeout(response, idle_timeout, self.model): @@ -223,7 +223,7 @@ class CollectProcessor(BaseProcessor): def __init__(self, model: str, token: str = ""): super().__init__(model, token) self.image_format = get_config("app.image_format") - self.filter_tags = get_config("grok.filter_tags") + self.filter_tags = get_config("chat.filter_tags") def _filter_content(self, content: str) -> str: """过滤内容中的特殊标签""" @@ -242,7 +242,7 @@ class CollectProcessor(BaseProcessor): response_id = "" fingerprint = "" content = "" - idle_timeout = get_config("grok.stream_idle_timeout") + idle_timeout = get_config("timeout.stream_idle_timeout") try: async for line in _with_idle_timeout(response, idle_timeout, self.model): diff --git a/app/services/grok/processors/image_processors.py b/app/services/grok/processors/image_processors.py index 8550108..42214c7 100644 --- a/app/services/grok/processors/image_processors.py +++ b/app/services/grok/processors/image_processors.py @@ -45,7 +45,7 @@ class ImageStreamProcessor(BaseProcessor): async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, None]: """处理流式响应""" final_images = [] - idle_timeout = get_config("grok.stream_idle_timeout") + idle_timeout = get_config("timeout.stream_idle_timeout") try: async for line in _with_idle_timeout(response, idle_timeout, self.model): @@ -160,7 +160,7 @@ class ImageCollectProcessor(BaseProcessor): async def process(self, response: AsyncIterable[bytes]) -> List[str]: """处理并收集图片""" images = [] - idle_timeout = get_config("grok.stream_idle_timeout") + idle_timeout = get_config("timeout.stream_idle_timeout") try: async for line in _with_idle_timeout(response, idle_timeout, self.model): diff --git a/app/services/grok/processors/video_processors.py b/app/services/grok/processors/video_processors.py index df8750c..ec39aae 100644 --- a/app/services/grok/processors/video_processors.py +++ b/app/services/grok/processors/video_processors.py @@ -32,7 +32,7 @@ class VideoStreamProcessor(BaseProcessor): self.video_format = str(get_config("app.video_format")).lower() if think is None: - self.show_think = get_config("grok.thinking") + self.show_think = get_config("chat.thinking") else: self.show_think = think @@ -68,7 +68,7 @@ class VideoStreamProcessor(BaseProcessor): async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, None]: """处理视频流式响应""" - idle_timeout = get_config("grok.video_idle_timeout") + idle_timeout = get_config("timeout.video_idle_timeout") try: async for line in _with_idle_timeout(response, idle_timeout, self.model): @@ -171,7 +171,7 @@ class VideoCollectProcessor(BaseProcessor): """处理并收集视频响应""" response_id = "" content = "" - idle_timeout = get_config("grok.video_idle_timeout") + idle_timeout = get_config("timeout.video_idle_timeout") try: async for line in _with_idle_timeout(response, idle_timeout, self.model): diff --git a/app/services/grok/services/assets.py b/app/services/grok/services/assets.py index 91a5270..d576b81 100644 --- a/app/services/grok/services/assets.py +++ b/app/services/grok/services/assets.py @@ -111,10 +111,10 @@ class ServiceConfig: @classmethod def from_settings(cls, proxy: Optional[str] = None): return cls( - proxy=proxy or get_config("grok.asset_proxy_url") or get_config("grok.base_proxy_url"), - timeout=get_config("grok.timeout"), - browser=get_config("grok.browser"), - user_agent=get_config("grok.user_agent"), + proxy=proxy or get_config("network.asset_proxy_url") or get_config("network.base_proxy_url"), + timeout=get_config("network.timeout"), + browser=get_config("security.browser"), + user_agent=get_config("security.user_agent"), ) def get_proxies(self) -> Optional[dict]: diff --git a/app/services/grok/services/chat.py b/app/services/grok/services/chat.py index 8d2aab7..99ea4ce 100644 --- a/app/services/grok/services/chat.py +++ b/app/services/grok/services/chat.py @@ -102,7 +102,7 @@ class ChatRequestBuilder: @staticmethod def build_headers(token: str) -> Dict[str, str]: """构造请求头""" - user_agent = get_config("grok.user_agent") + user_agent = get_config("security.user_agent") headers = { "Accept": "*/*", "Accept-Encoding": "gzip, deflate, br, zstd", @@ -142,7 +142,7 @@ class ChatRequestBuilder: merged_attachments.extend(image_attachments) payload = { - "temporary": get_config("grok.temporary"), + "temporary": get_config("chat.temporary"), "modelName": model, "message": message, "fileAttachments": merged_attachments, @@ -160,7 +160,7 @@ class ChatRequestBuilder: "modelConfigOverride": {"modelMap": {}}, "requestModelDetails": {"modelId": model}, }, - "disableMemory": get_config("grok.disable_memory"), + "disableMemory": get_config("chat.disable_memory"), "deviceEnvInfo": { "darkModeEnabled": False, "devicePixelRatio": 2, @@ -181,27 +181,27 @@ class GrokChatService: """Grok API 调用服务""" def __init__(self, proxy: str = None): - self.proxy = proxy or get_config("grok.base_proxy_url") + self.proxy = proxy or get_config("network.base_proxy_url") async def chat(self, token: str, message: str, model: str = "grok-3", mode: str = None, stream: bool = None, file_attachments: List[str] = None, image_attachments: List[str] = None, raw_payload: Dict[str, Any] = None): """发送聊天请求""" if stream is None: - stream = get_config("grok.stream") + stream = get_config("chat.stream") headers = ChatRequestBuilder.build_headers(token) payload = raw_payload if raw_payload is not None else ChatRequestBuilder.build_payload( message, model, mode, file_attachments, image_attachments ) proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None - timeout = get_config("grok.timeout") + timeout = get_config("network.timeout") logger.debug(f"Chat request: model={model}, mode={mode}, stream={stream}, attachments={len(file_attachments or [])}") # 建立连接 async def establish_connection(): - browser = get_config("grok.browser") + browser = get_config("security.browser") session = AsyncSession(impersonate=browser) try: response = await session.post( @@ -303,7 +303,7 @@ class GrokChatService: finally: await upload_service.close() - stream = request.stream if request.stream is not None else get_config("grok.stream") + stream = request.stream if request.stream is not None else get_config("chat.stream") response = await self.chat( token, message, grok_model, mode, stream, @@ -340,7 +340,7 @@ class ChatService: # 解析参数 think = {"enabled": True, "disabled": False}.get(thinking) - is_stream = stream if stream is not None else get_config("grok.stream") + is_stream = stream if stream is not None else get_config("chat.stream") # 构造请求 chat_request = ChatRequest(model=model, messages=messages, stream=is_stream, think=think) diff --git a/app/services/grok/services/image.py b/app/services/grok/services/image.py index 9e51207..b94feb6 100644 --- a/app/services/grok/services/image.py +++ b/app/services/grok/services/image.py @@ -32,7 +32,7 @@ class ImageService: self._url_pattern = re.compile(r"/images/([a-f0-9-]+)\.(png|jpg|jpeg)") def _resolve_proxy(self) -> tuple[aiohttp.BaseConnector, Optional[str]]: - proxy_url = get_config("grok.base_proxy_url") + proxy_url = get_config("network.base_proxy_url") if not proxy_url: return aiohttp.TCPConnector(ssl=self._ssl_context), None @@ -46,7 +46,7 @@ class ImageService: def _get_ws_headers(self, token: str) -> Dict[str, str]: cookie = build_sso_cookie(token, include_rw=True) - user_agent = get_config("grok.user_agent") + user_agent = get_config("security.user_agent") return { "Cookie": cookie, "Origin": "https://grok.com", @@ -61,7 +61,7 @@ class ImageService: return match.group(1) if match else None def _is_final_image(self, url: str, blob_size: int) -> bool: - return (url or "").lower().endswith((".jpg", ".jpeg")) and blob_size > get_config("grok.image_ws_final_min_bytes") + return (url or "").lower().endswith((".jpg", ".jpeg")) and blob_size > get_config("image.image_ws_final_min_bytes") def _classify_image(self, url: str, blob: str) -> Optional[Dict[str, object]]: if not url or not blob: @@ -71,7 +71,7 @@ class ImageService: blob_size = len(blob) is_final = self._is_final_image(url, blob_size) - stage = "final" if is_final else ("medium" if blob_size > get_config("grok.image_ws_medium_min_bytes") else "preview") + stage = "final" if is_final else ("medium" if blob_size > get_config("image.image_ws_medium_min_bytes") else "preview") return { "type": "image", @@ -122,8 +122,8 @@ class ImageService: ) -> AsyncGenerator[Dict[str, object], None]: request_id = str(uuid.uuid4()) headers = self._get_ws_headers(token) - timeout = float(get_config("grok.timeout")) - blocked_seconds = float(get_config("grok.image_ws_blocked_seconds")) + timeout = float(get_config("network.timeout")) + blocked_seconds = float(get_config("image.image_ws_blocked_seconds")) try: connector, proxy = self._resolve_proxy() diff --git a/app/services/grok/services/media.py b/app/services/grok/services/media.py index 39b0849..10c4ba4 100644 --- a/app/services/grok/services/media.py +++ b/app/services/grok/services/media.py @@ -43,14 +43,14 @@ class VideoService: """视频生成服务""" def __init__(self, proxy: str = None): - self.proxy = proxy or get_config("grok.base_proxy_url") - self.timeout = get_config("grok.timeout") + self.proxy = proxy or get_config("network.base_proxy_url") + self.timeout = get_config("network.timeout") def _build_headers( self, token: str, referer: str = "https://grok.com/imagine" ) -> dict: """构建请求头""" - user_agent = get_config("grok.user_agent") + user_agent = get_config("security.user_agent") headers = { "Accept": "*/*", "Accept-Encoding": "gzip, deflate, br, zstd", @@ -103,7 +103,7 @@ class VideoService: CREATE_POST_API, headers=headers, json=payload, - impersonate=get_config("grok.browser"), + impersonate=get_config("security.browser"), timeout=30, proxies=self._build_proxies(), ) @@ -172,7 +172,7 @@ class VideoService: headers = self._build_headers(token) payload = self._build_payload(prompt, post_id, aspect_ratio, video_length, resolution_name, preset) - session = AsyncSession(impersonate=get_config("grok.browser")) + session = AsyncSession(impersonate=get_config("security.browser")) response = await session.post( CHAT_API, headers=headers, @@ -267,7 +267,7 @@ class VideoService: ) think = {"enabled": True, "disabled": False}.get(thinking) - is_stream = stream if stream is not None else get_config("grok.stream") + is_stream = stream if stream is not None else get_config("chat.stream") # 提取内容 from app.services.grok.services.chat import MessageExtractor diff --git a/app/services/grok/services/nsfw.py b/app/services/grok/services/nsfw.py index 169ed4d..fa02b8f 100644 --- a/app/services/grok/services/nsfw.py +++ b/app/services/grok/services/nsfw.py @@ -37,8 +37,8 @@ class NSFWService: """NSFW 模式服务""" def __init__(self, proxy: str = None): - self.proxy = proxy or get_config("grok.base_proxy_url") - self.timeout = float(get_config("grok.timeout")) + self.proxy = proxy or get_config("network.base_proxy_url") + self.timeout = float(get_config("network.timeout")) def _build_proxies(self) -> Optional[dict]: """构建代理配置""" @@ -60,7 +60,7 @@ 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") + user_agent = get_config("security.user_agent") return { "accept": "*/*", "content-type": "application/grpc-web+proto", @@ -75,7 +75,7 @@ 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") + user_agent = get_config("security.user_agent") return { "accept": "*/*", "content-type": "application/json", @@ -97,9 +97,7 @@ class NSFWService: protobuf = b"\x0a\x02\x10\x01\x12" + bytes([len(inner)]) + inner return encode_grpc_web_payload(protobuf) - async def _set_birth_date( - self, session: AsyncSession, token: str - ) -> tuple[bool, int, Optional[str]]: + async def _set_birth_date(self, session: AsyncSession, token: str) -> tuple[bool, int, Optional[str]]: """设置出生日期""" headers = self._build_birth_headers(token) payload = {"birthDate": self._random_birth_date()} @@ -125,7 +123,7 @@ class NSFWService: logger.debug(f"NSFW payload: len={len(payload)} hex={payload.hex()}") try: - browser = get_config("grok.browser") + browser = get_config("security.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 bf3d5cb..adb04b3 100644 --- a/app/services/grok/services/usage.py +++ b/app/services/grok/services/usage.py @@ -23,12 +23,12 @@ class UsageService: """用量查询服务""" def __init__(self, proxy: str = None): - self.proxy = proxy or get_config("grok.base_proxy_url") - self.timeout = get_config("grok.timeout") + self.proxy = proxy or get_config("network.base_proxy_url") + self.timeout = get_config("network.timeout") def _build_headers(self, token: str) -> dict: """构建请求头""" - user_agent = get_config("grok.user_agent") + user_agent = get_config("security.user_agent") headers = { "Accept": "*/*", "Accept-Encoding": "gzip, deflate, br, zstd", @@ -97,7 +97,7 @@ class UsageService: try: headers = self._build_headers(token) payload = {"requestKind": "DEFAULT", "modelName": model_name} - browser = get_config("grok.browser") + browser = get_config("security.browser") async with AsyncSession() as session: response = await session.post( diff --git a/app/services/grok/services/voice.py b/app/services/grok/services/voice.py index 6c231ae..ae003f0 100644 --- a/app/services/grok/services/voice.py +++ b/app/services/grok/services/voice.py @@ -18,7 +18,7 @@ class VoiceService: """Voice Mode Service (LiveKit)""" def __init__(self, proxy: str = None): - self.proxy = proxy or get_config("grok.base_proxy_url") + self.proxy = proxy or get_config("network.base_proxy_url") async def get_token( self, @@ -42,8 +42,8 @@ class VoiceService: proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None try: - browser = get_config("grok.browser") - timeout = get_config("grok.timeout") + browser = get_config("security.browser") + timeout = get_config("network.timeout") async with AsyncSession(impersonate=browser) as session: response = await session.post( LIVEKIT_TOKEN_API, diff --git a/app/services/grok/utils/headers.py b/app/services/grok/utils/headers.py index 146e704..7a5e1c2 100644 --- a/app/services/grok/utils/headers.py +++ b/app/services/grok/utils/headers.py @@ -17,7 +17,7 @@ def _normalize_token(token: str) -> str: def build_sso_cookie(token: str, include_rw: bool = False) -> str: token = _normalize_token(token) - cf = get_config("grok.cf_clearance") + cf = get_config("security.cf_clearance") cookie = f"sso={token}" if include_rw: cookie = f"{cookie}; sso-rw={token}" diff --git a/app/services/grok/utils/retry.py b/app/services/grok/utils/retry.py index 2cd52c5..7fa7eba 100644 --- a/app/services/grok/utils/retry.py +++ b/app/services/grok/utils/retry.py @@ -23,17 +23,17 @@ class RetryContext: def __init__(self): self.attempt = 0 - self.max_retry = int(get_config("grok.max_retry")) - self.retry_codes = get_config("grok.retry_status_codes") + self.max_retry = int(get_config("retry.max_retry")) + self.retry_codes = get_config("retry.retry_status_codes") self.last_error = None self.last_status = None self.total_delay = 0.0 - self.retry_budget = float(get_config("grok.retry_budget")) + self.retry_budget = float(get_config("retry.retry_budget")) # 退避参数 - self.backoff_base = float(get_config("grok.retry_backoff_base")) - self.backoff_factor = float(get_config("grok.retry_backoff_factor")) - self.backoff_max = float(get_config("grok.retry_backoff_max")) + self.backoff_base = float(get_config("retry.retry_backoff_base")) + self.backoff_factor = float(get_config("retry.retry_backoff_factor")) + self.backoff_max = float(get_config("retry.retry_backoff_max")) # decorrelated jitter 状态 self._last_delay = self.backoff_base diff --git a/app/services/grok/utils/statsig.py b/app/services/grok/utils/statsig.py index bf78741..c2cd15f 100644 --- a/app/services/grok/utils/statsig.py +++ b/app/services/grok/utils/statsig.py @@ -30,7 +30,7 @@ class StatsigService: Returns: Base64 编码的 ID """ - dynamic = get_config("grok.dynamic_statsig") + dynamic = get_config("chat.dynamic_statsig") if not dynamic: return "ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk=" diff --git a/app/static/config/config.html b/app/static/config/config.html index 7fb207f..6fdff9e 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 bcb8036..f0282a7 100644 --- a/app/static/config/config.js +++ b/app/static/config/config.js @@ -26,6 +26,7 @@ const NUMERIC_FIELDS = new Set([ 'video_idle_timeout', 'image_ws_blocked_seconds', 'image_ws_final_min_bytes', + 'image_ws_medium_min_bytes', 'nsfw_max_concurrent', 'nsfw_batch_size', 'nsfw_max_tokens' @@ -40,44 +41,60 @@ const LOCALE_MAP = { "image_format": { title: "图片格式", desc: "生成的图片格式(url 或 base64)。" }, "video_format": { title: "视频格式", desc: "生成的视频格式(html 或 url,url 为处理后的链接)。" } }, - "grok": { - "label": "Grok 设置", + "network": { + "label": "网络配置", + "timeout": { title: "请求超时", desc: "请求 Grok 服务的超时时间(秒)。" }, + "base_proxy_url": { title: "基础代理 URL", desc: "代理请求到 Grok 官网的基础服务地址。" }, + "asset_proxy_url": { title: "资源代理 URL", desc: "代理请求到 Grok 官网的静态资源(图片/视频)地址。" } + }, + "security": { + "label": "反爬虫验证", + "cf_clearance": { title: "CF Clearance", desc: "Cloudflare Clearance Cookie,用于绕过反爬虫验证。" }, + "browser": { title: "浏览器指纹", desc: "curl_cffi 浏览器指纹标识(如 chrome136)。" }, + "user_agent": { title: "User-Agent", desc: "HTTP 请求的 User-Agent 字符串,需与浏览器指纹匹配。" } + }, + "chat": { + "label": "对话配置", "temporary": { title: "临时对话", desc: "是否启用临时对话模式。" }, - "disable_memory": { title: "禁用记忆", desc: "禁用 Grok 记忆功能,以防止响应中出现不相关上下文" }, + "disable_memory": { title: "禁用记忆", desc: "禁用 Grok 记忆功能,以防止响应中出现不相关上下文。" }, "stream": { title: "流式响应", desc: "是否默认启用流式输出。" }, "thinking": { title: "思维链", desc: "是否启用模型思维链输出。" }, "dynamic_statsig": { title: "动态指纹", desc: "是否启用动态生成 Statsig 值。" }, - "filter_tags": { title: "过滤标签", desc: "自动过滤 Grok 响应中的特殊标签。" }, - "timeout": { title: "超时时间", desc: "请求 Grok 服务的超时时间(秒)。" }, - "base_proxy_url": { title: "基础代理 URL", desc: "代理请求到 Grok 官网的基础服务地址。" }, - "asset_proxy_url": { title: "资源代理 URL", desc: "代理请求到 Grok 官网的静态资源(图片/视频)地址。" }, - "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。" }, - "image_ws_final_min_bytes": { title: "Imagine WebSocket 最终图最小字节", desc: "判定最终图的最小字节数(通常 JPG > 100KB)。" }, - "max_retry": { title: "最大重试", desc: "请求 Grok 服务失败时的最大重试次数。" }, + "filter_tags": { title: "过滤标签", desc: "自动过滤 Grok 响应中的特殊标签。" } + }, + "retry": { + "label": "重试策略", + "max_retry": { title: "最大重试次数", desc: "请求 Grok 服务失败时的最大重试次数。" }, "retry_status_codes": { title: "重试状态码", desc: "触发重试的 HTTP 状态码列表。" }, "retry_backoff_base": { title: "退避基数", desc: "重试退避的基础延迟(秒)。" }, "retry_backoff_factor": { title: "退避倍率", desc: "重试退避的指数放大系数。" }, "retry_backoff_max": { title: "退避上限", desc: "单次重试等待的最大延迟(秒)。" }, - "retry_budget": { title: "退避预算", desc: "单次请求的最大重试总耗时(秒)。" }, + "retry_budget": { title: "退避预算", desc: "单次请求的最大重试总耗时(秒)。" } + }, + "timeout": { + "label": "超时配置", "stream_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: "" } + "video_idle_timeout": { title: "视频空闲超时", desc: "视频生成空闲超时(秒),超过将断开。" } + }, + "image": { + "label": "图片生成", + "image_ws": { title: "WebSocket 生成", desc: "启用后 /v1/images/generations 走 WebSocket 直连。" }, + "image_ws_nsfw": { title: "NSFW 模式", desc: "WebSocket 请求是否启用 NSFW。" }, + "image_ws_blocked_seconds": { title: "Blocked 阈值", desc: "收到中等图后超过该秒数仍无最终图则判定 blocked。" }, + "image_ws_final_min_bytes": { title: "最终图最小字节", desc: "判定最终图的最小字节数(通常 JPG > 100KB)。" }, + "image_ws_medium_min_bytes": { title: "中等图最小字节", desc: "判定中等质量图的最小字节数。" } }, "token": { - "label": "Token 池设置", + "label": "Token 池管理", "auto_refresh": { title: "自动刷新", desc: "是否开启 Token 自动刷新机制。" }, - "refresh_interval_hours": { title: "刷新间隔", desc: "Token 刷新的时间间隔(小时)。" }, + "refresh_interval_hours": { title: "刷新间隔", desc: "普通 Token 刷新的时间间隔(小时)。" }, "super_refresh_interval_hours": { title: "Super 刷新间隔", desc: "Super Token 刷新的时间间隔(小时)。" }, "fail_threshold": { title: "失败阈值", desc: "单个 Token 连续失败多少次后被标记为不可用。" }, "save_delay_ms": { title: "保存延迟", desc: "Token 变更合并写入的延迟(毫秒)。" }, - "reload_interval_sec": { title: "一致性刷新", desc: "多 worker 场景下 Token 状态刷新间隔(秒)。" } + "reload_interval_sec": { title: "同步间隔", desc: "多 worker 场景下 Token 状态刷新间隔(秒)。" } }, "cache": { - "label": "缓存设置", + "label": "缓存管理", "enable_auto_clean": { title: "自动清理", desc: "是否启用缓存自动清理,开启后按上限自动回收。" }, "limit_mb": { title: "清理阈值", desc: "缓存大小阈值(MB),超过阈值会触发清理。" } }, @@ -97,11 +114,9 @@ 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_DESCRIPTIONS = { + "security": "配置不正确将导致 403 错误。服务首次请求 Grok 时的 IP 必须与获取 CF Clearance 时的 IP 一致,后续服务器请求 IP 变化不会导致 403。" }; const SECTION_ORDER = new Map(Object.keys(LOCALE_MAP).map((key, index) => [key, index])); @@ -250,24 +265,29 @@ function renderConfig(data) { const localeSection = LOCALE_MAP[section]; const keyOrder = localeSection ? new Map(Object.keys(localeSection).map((k, i) => [k, i])) : null; - // 分离反爬虫字段和普通字段 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)) : []; - // 渲染普通字段 - if (normalKeys.length > 0) { + if (allKeys.length > 0) { const card = document.createElement('div'); card.className = 'config-section'; const header = document.createElement('div'); header.innerHTML = `
${getSectionLabel(section)}
`; + + // 添加部分说明(如果有) + if (SECTION_DESCRIPTIONS[section]) { + const descP = document.createElement('p'); + descP.className = 'text-[var(--accents-4)] text-sm mt-1 mb-4'; + descP.textContent = SECTION_DESCRIPTIONS[section]; + header.appendChild(descP); + } + card.appendChild(header); const grid = document.createElement('div'); grid.className = 'config-grid'; - normalKeys.forEach(key => { + allKeys.forEach(key => { const fieldCard = buildFieldCard(section, key, items[key]); grid.appendChild(fieldCard); }); @@ -277,28 +297,6 @@ function renderConfig(data) { fragment.appendChild(card); } } - - // 渲染反爬虫验证分组(仅 grok section) - if (antibotKeys.length > 0) { - const card = document.createElement('div'); - card.className = 'config-section'; - - const header = document.createElement('div'); - header.innerHTML = `
${ANTIBOT_GROUP.label}
-

${ANTIBOT_GROUP.desc}

`; - card.appendChild(header); - - 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); - } }); container.appendChild(fragment); diff --git a/config.defaults.toml b/config.defaults.toml index d425cb1..cf95fdf 100644 --- a/config.defaults.toml +++ b/config.defaults.toml @@ -1,58 +1,139 @@ -[grok] -temporary = true -stream = true -thinking = true -dynamic_statsig = true -filter_tags = ["xaiartifact","xai:tool_usage_card","grok:render"] -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 -image_ws_blocked_seconds = 15 -image_ws_final_min_bytes = 100000 -image_ws_nsfw = true -retry_backoff_base = 0.5 -retry_backoff_factor = 2.0 -retry_backoff_max = 30.0 -retry_budget = 90.0 -stream_idle_timeout = 120.0 -video_idle_timeout = 90.0 - +# ==================== 应用设置 ==================== [app] +# 应用访问地址(用于生成文件链接) app_url = "http://127.0.0.1:8000" +# 后台管理密码 app_key = "grok2api" +# API 调用密钥(可选) api_key = "" +# 生成图片的格式(url 或 base64) image_format = "url" +# 生成视频的格式(html 或 url) video_format = "html" + +# ==================== 网络配置 ==================== +[network] +# 请求超时时间(秒) +timeout = 120 +# 基础代理地址(代理到 Grok 官网) +base_proxy_url = "" +# 资源代理地址(代理静态资源如图片/视频) +asset_proxy_url = "" + + +# ==================== 反爬虫验证 ==================== +[security] +# Cloudflare Clearance Cookie +cf_clearance = "" +# curl_cffi 浏览器指纹 +browser = "chrome136" +# User-Agent 字符串 +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" + + +# ==================== 对话配置 ==================== +[chat] +# 是否启用临时对话模式 +temporary = true +# 是否禁用 Grok 记忆功能 +disable_memory = true +# 是否默认启用流式响应 +stream = true +# 是否启用思维链输出 +thinking = true +# 是否动态生成 Statsig 指纹 +dynamic_statsig = true +# 过滤的特殊标签列表 +filter_tags = ["xaiartifact","xai:tool_usage_card","grok:render"] + + +# ==================== 重试策略 ==================== +[retry] +# 最大重试次数 +max_retry = 3 +# 触发重试的 HTTP 状态码 +retry_status_codes = [401,429,403] +# 退避基础延迟(秒) +retry_backoff_base = 0.5 +# 退避倍率 +retry_backoff_factor = 2.0 +# 单次重试最大延迟(秒) +retry_backoff_max = 30.0 +# 总重试预算时间(秒) +retry_budget = 90.0 + + +# ==================== 超时配置 ==================== +[timeout] +# 流式响应空闲超时(秒) +stream_idle_timeout = 120.0 +# 视频生成空闲超时(秒) +video_idle_timeout = 90.0 + + +# ==================== 图片生成 ==================== +[image] +# 是否启用 WebSocket 直连生成图片 +image_ws = true +# WebSocket 生成时是否启用 NSFW +image_ws_nsfw = true +# 收到中等质量图后等待最终图的超时秒数 +image_ws_blocked_seconds = 15 +# 判定为最终图的最小字节数 +image_ws_final_min_bytes = 100000 +# 判定为中等质量图的最小字节数 +image_ws_medium_min_bytes = 30000 + + +# ==================== Token 池管理 ==================== [token] +# 是否启用 Token 自动刷新 auto_refresh = true +# 普通 Token 刷新间隔(小时) refresh_interval_hours = 8 +# Super Token 刷新间隔(小时) super_refresh_interval_hours = 2 +# Token 连续失败阈值 fail_threshold = 5 +# Token 变更保存延迟(毫秒) save_delay_ms = 500 +# 多 worker 状态同步间隔(秒) reload_interval_sec = 30 + +# ==================== 缓存管理 ==================== [cache] +# 是否启用自动清理 enable_auto_clean = true +# 缓存大小上限(MB) limit_mb = 1024 + +# ==================== 并发性能 ==================== [performance] +# Assets 批量处理并发上限 assets_max_concurrent = 25 -media_max_concurrent = 50 -usage_max_concurrent = 25 +# Assets 批量删除大小 assets_delete_batch_size = 10 +# Assets 单批处理数量 assets_batch_size = 10 +# Assets 单次最大处理数量 assets_max_tokens = 1000 + +# Media 生成并发上限 +media_max_concurrent = 50 + +# Token 用量刷新并发上限 +usage_max_concurrent = 25 +# Token 用量刷新批次大小 usage_batch_size = 50 +# Token 用量单次最大数量 usage_max_tokens = 1000 + +# NSFW 批量开启并发上限 nsfw_max_concurrent = 10 +# NSFW 批量开启批次大小 nsfw_batch_size = 50 +# NSFW 单次最大数量 nsfw_max_tokens = 1000 diff --git a/data/config.toml b/data/config.toml index 22296cf..e51f56f 100644 --- a/data/config.toml +++ b/data/config.toml @@ -1,53 +1,46 @@ -[grok] -browser = "chrome136" +[app] +app_url = "http://127.0.0.1:8000" +app_key = "grok2api" +api_key = "" +image_format = "url" +video_format = "html" + +[network] timeout = 120 -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" base_proxy_url = "" asset_proxy_url = "" + +[security] 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" + +[chat] temporary = true disable_memory = true stream = true thinking = true +dynamic_statsig = true filter_tags = ["xaiartifact","xai:tool_usage_card","grok:render"] + +[retry] max_retry = 3 retry_status_codes = [401,429,403] retry_backoff_base = 0.5 retry_backoff_factor = 2.0 retry_backoff_max = 30.0 retry_budget = 90.0 + +[timeout] stream_idle_timeout = 120.0 video_idle_timeout = 90.0 + +[image] +image_ws = true +image_ws_nsfw = true image_ws_blocked_seconds = 15 image_ws_final_min_bytes = 100000 image_ws_medium_min_bytes = 30000 -dynamic_statsig = true -image_ws = true -image_ws_nsfw = true - -[app] -app_url = "http://127.0.0.1:8000" -image_format = "url" -video_format = "html" -app_key = "grok2api" -api_key = "" - -[performance] -assets_max_concurrent = 25 -media_max_concurrent = 50 -usage_max_concurrent = 25 -assets_delete_batch_size = 10 -assets_batch_size = 10 -assets_max_tokens = 1000 -usage_batch_size = 50 -usage_max_tokens = 1000 -nsfw_max_concurrent = 10 -nsfw_batch_size = 50 -nsfw_max_tokens = 1000 - -[cache] -enable_auto_clean = true -limit_mb = 1024 [token] auto_refresh = true @@ -56,3 +49,43 @@ super_refresh_interval_hours = 2 fail_threshold = 5 save_delay_ms = 200 reload_interval_sec = 30 + +[cache] +enable_auto_clean = true +limit_mb = 1024 + +[performance] +assets_max_concurrent = 25 +assets_delete_batch_size = 10 +assets_batch_size = 10 +assets_max_tokens = 1000 +media_max_concurrent = 50 +usage_max_concurrent = 25 +usage_batch_size = 50 +usage_max_tokens = 1000 +nsfw_max_concurrent = 10 +nsfw_batch_size = 50 +nsfw_max_tokens = 1000 + +[grok] +temporary = true +stream = true +thinking = true +dynamic_statsig = true +filter_tags = ["xaiartifact","xai:tool_usage_card","grok:render"] +timeout = 120 +base_proxy_url = "" +asset_proxy_url = "" +cf_clearance = "" +max_retry = 3 +retry_status_codes = [401,429,403] +image_ws = true +image_ws_blocked_seconds = 15 +image_ws_final_min_bytes = 100000 +image_ws_nsfw = true +retry_backoff_base = 0.5 +retry_backoff_factor = 2.0 +retry_backoff_max = 30.0 +retry_budget = 90.0 +stream_idle_timeout = 120.0 +video_idle_timeout = 90.0 diff --git a/docs/README.en.md b/docs/README.en.md index 34bc0e5..66d5c0f 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -1,6 +1,6 @@ # Grok2API -[中文](README.md) | **English** +[中文](../README.md) | **English** > [!NOTE] > This project is for learning and research only. You must comply with Grok's Terms of Use and applicable laws. Do not use it for illegal purposes. @@ -194,6 +194,10 @@ Config file: `data/config.toml` > In production or behind a reverse proxy, make sure `app.app_url` is set to the public URL. > Otherwise file links may be incorrect or return 403. +> [!TIP] +> **v2.0 Config Upgrade**: Existing users will have their config **auto-migrated** to the new structure upon update. +> Custom values from the old `[grok]` section will be automatically mapped to the corresponding new sections. + | Module | Field | Key | Description | Default | | :--- | :--- | :--- | :--- | :--- | | **app** | `app_url` | App URL | External access URL for Grok2API (used for file links). | `http://127.0.0.1:8000` | @@ -201,46 +205,50 @@ Config file: `data/config.toml` | | `api_key` | API key | Token for calling Grok2API (optional). | `""` | | | `image_format` | Image format | Output image format (`url` or `base64`). | `url` | | | `video_format` | Video format | Output video format (html tag or processed url). | `html` | -| **grok** | `temporary` | Temporary chat | Enable temporary conversation mode. | `true` | +| **network** | `timeout` | Request timeout | Timeout for Grok requests (seconds). | `120` | +| | `base_proxy_url` | Base proxy URL | Base service address proxying Grok official site. | `""` | +| | `asset_proxy_url` | Asset proxy URL | Proxy URL for Grok static assets (images/videos). | `""` | +| **security** | `cf_clearance` | CF Clearance | Cloudflare clearance cookie for bypassing anti-bot. | `""` | +| | `browser` | Browser fingerprint | curl_cffi browser fingerprint (e.g. chrome136). | `chrome136` | +| | `user_agent` | User-Agent | HTTP User-Agent string. | `Mozilla/5.0 (Macintosh; ...)` | +| **chat** | `temporary` | Temporary chat | Enable temporary conversation mode. | `true` | +| | `disable_memory` | Disable memory | Disable Grok memory to prevent irrelevant context. | `true` | | | `stream` | Streaming | Enable streaming by default. | `true` | | | `thinking` | Thinking chain | Enable model thinking output. | `true` | | | `dynamic_statsig` | Dynamic fingerprint | Enable dynamic Statsig value generation. | `true` | | | `filter_tags` | Filter tags | Auto-filter special tags in Grok responses. | `["xaiartifact", "xai:tool_usage_card", "grok:render"]` | -| | `timeout` | Timeout | Timeout for Grok requests (seconds). | `120` | -| | `base_proxy_url` | Base proxy URL | Base service address proxying Grok official site. | `""` | -| | `asset_proxy_url` | Asset proxy URL | Proxy URL for Grok static assets (images/videos). | `""` | -| | `cf_clearance` | CF Clearance | Cloudflare clearance cookie for verification. | `""` | -| | `max_retry` | Max retries | Max retries on Grok request failure. | `3` | +| **retry** | `max_retry` | Max retries | Max retries on Grok request failure. | `3` | | | `retry_status_codes` | Retry status codes | HTTP status codes that trigger retry. | `[401, 429, 403]` | | | `retry_backoff_base` | Backoff base | Base delay for retry backoff (seconds). | `0.5` | | | `retry_backoff_factor` | Backoff factor | Exponential multiplier for retry backoff. | `2.0` | | | `retry_backoff_max` | Backoff max | Max wait per retry (seconds). | `30.0` | | | `retry_budget` | Backoff budget | Max total retry time per request (seconds). | `90.0` | -| | `stream_idle_timeout` | Stream idle timeout | Idle timeout for streaming responses (seconds). | `45.0` | +| **timeout** | `stream_idle_timeout` | Stream idle timeout | Idle timeout for streaming responses (seconds). | `120.0` | | | `video_idle_timeout` | Video idle timeout | Idle timeout for video generation (seconds). | `90.0` | -| | `image_ws` | Image WS generation | When enabled, `/v1/images/generations` uses WebSocket. | `false` | -| | `image_ws_blocked_seconds` | WS blocked threshold | Mark blocked if no final image after this many seconds post-medium. | `15` | -| | `image_ws_final_min_bytes` | WS final min bytes | Minimum bytes to treat an image as final (JPG usually > 100KB). | `100000` | -| | `image_ws_nsfw` | WS NSFW | Enable NSFW in WS requests. | `true` | +| **image** | `image_ws` | WebSocket generation | When enabled, `/v1/images/generations` uses WebSocket. | `true` | +| | `image_ws_nsfw` | NSFW mode | Enable NSFW in WebSocket requests. | `true` | +| | `image_ws_blocked_seconds` | Blocked threshold | Mark blocked if no final image after this many seconds post-medium. | `15` | +| | `image_ws_final_min_bytes` | Final min bytes | Minimum bytes to treat an image as final (JPG usually > 100KB). | `100000` | +| | `image_ws_medium_min_bytes` | Medium min bytes | Minimum bytes for medium quality image. | `30000` | | **token** | `auto_refresh` | Auto refresh | Enable automatic token refresh. | `true` | -| | `refresh_interval_hours` | Refresh interval | Token refresh interval (hours). | `8` | +| | `refresh_interval_hours` | Refresh interval | Regular token refresh interval (hours). | `8` | | | `super_refresh_interval_hours` | Super refresh interval | Super token refresh interval (hours). | `2` | | | `fail_threshold` | Failure threshold | Consecutive failures before a token is disabled. | `5` | | | `save_delay_ms` | Save delay | Debounced save delay for token changes (ms). | `500` | -| | `reload_interval_sec` | Consistency refresh | Token state refresh interval in multi-worker setups (sec). | `30` | +| | `reload_interval_sec` | Sync interval | Token state refresh interval in multi-worker setups (sec). | `30` | | **cache** | `enable_auto_clean` | Auto clean | Enable cache auto clean; cleanup when exceeding limit. | `true` | | | `limit_mb` | Cleanup threshold | Cache size threshold (MB) that triggers cleanup. | `1024` | -| **performance** | `nsfw_max_concurrent` | NSFW enable concurrency | Concurrency cap for enabling NSFW in batch. Recommended 10. | `10` | +| **performance** | `media_max_concurrent` | Media concurrency | Concurrency cap for video/media generation. Recommended 50. | `50` | +| | `assets_max_concurrent` | Assets concurrency | Concurrency cap for batch asset find/delete. Recommended 25. | `25` | +| | `assets_batch_size` | Assets batch size | Batch size for asset find/delete. Recommended 10. | `10` | +| | `assets_max_tokens` | Assets max tokens | Max tokens per asset find/delete batch. Recommended 1000. | `1000` | +| | `assets_delete_batch_size` | Assets delete batch | Batch concurrency for single-account asset deletion. Recommended 10. | `10` | +| | `usage_max_concurrent` | Token refresh concurrency | Concurrency cap for batch usage refresh. Recommended 25. | `25` | +| | `usage_batch_size` | Token refresh batch size | Batch size for usage refresh. Recommended 50. | `50` | +| | `usage_max_tokens` | Token refresh max tokens | Max tokens per usage refresh batch. Recommended 1000. | `1000` | +| | `nsfw_max_concurrent` | NSFW enable concurrency | Concurrency cap for enabling NSFW in batch. Recommended 10. | `10` | | | `nsfw_batch_size` | NSFW enable batch size | Batch size for enabling NSFW. Recommended 50. | `50` | | | `nsfw_max_tokens` | NSFW enable max tokens | Max tokens per NSFW batch to avoid mistakes. Recommended 1000. | `1000` | -| | `usage_max_concurrent` | Token usage refresh concurrency | Concurrency cap for batch usage refresh. Recommended 25. | `25` | -| | `usage_batch_size` | Token usage refresh batch size | Batch size for usage refresh. Recommended 50. | `50` | -| | `usage_max_tokens` | Token usage refresh max tokens | Max tokens per usage refresh batch. Recommended 1000. | `1000` | -| | `assets_max_concurrent` | Online assets find/delete concurrency | Concurrency cap for online asset find/delete. Recommended 25. | `25` | -| | `assets_batch_size` | Online assets find/delete batch size | Batch size for online asset find/delete. Recommended 10. | `10` | -| | `assets_max_tokens` | Online assets find/delete max tokens | Max tokens per online asset find/delete batch. Recommended 1000. | `1000` | -| | `assets_delete_batch_size` | Online assets delete batch | Batch concurrency for online asset deletion. Recommended 10. | `10` | -| | `media_max_concurrent` | Media concurrency | Concurrency cap for video/media generation. Recommended 50. | `50` |
diff --git a/readme.md b/readme.md index 90ff53a..bf8ea55 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # Grok2API -**中文** | [English](README.en.md) +**中文** | [English](docs/README.en.md) > [!NOTE] > 本项目仅供学习与研究,使用者必须在遵循 Grok 的 **使用条款** 以及 **法律法规** 的情况下使用,不得用于非法用途。 @@ -223,6 +223,10 @@ curl http://localhost:8000/v1/images/edits \ > 生产环境或反向代理部署时,请确保 `app.app_url` 配置为对外可访问的完整 URL, > 否则可能出现文件访问链接不正确或 403 等问题。 +> [!TIP] +> **v2.0 配置结构升级**:旧版本用户更新后,配置会**自动迁移**到新结构,无需手动修改。 +> 旧的 `[grok]` 配置节中的自定义值会自动映射到对应的新配置节。 + | 模块 | 字段 | 配置名 | 说明 | 默认值 | | :-------------------- | :--------------------------- | :----------- | :--------------------------------------------------- | :-------------------------------------------------------- | | **app** | `app_url` | 应用地址 | 当前 Grok2API 服务的外部访问 URL,用于文件链接访问。 | `http://127.0.0.1:8000` | @@ -230,46 +234,50 @@ curl http://localhost:8000/v1/images/edits \ | | `api_key` | API 密钥 | 调用 Grok2API 服务的 Token(可选)。 | `""` | | | `image_format` | 图片格式 | 生成的图片格式(url 或 base64)。 | `url` | | | `video_format` | 视频格式 | 生成的视频格式(html 或 url,url 为处理后的链接)。 | `html` | -| **grok** | `temporary` | 临时对话 | 是否启用临时对话模式。 | `true` | +| **network** | `timeout` | 请求超时 | 请求 Grok 服务的超时时间(秒)。 | `120` | +| | `base_proxy_url` | 基础代理 URL | 代理请求到 Grok 官网的基础服务地址。 | `""` | +| | `asset_proxy_url` | 资源代理 URL | 代理请求到 Grok 官网的静态资源(图片/视频)地址。 | `""` | +| **security** | `cf_clearance` | CF Clearance | Cloudflare 验证 Cookie,用于绕过反爬虫验证。 | `""` | +| | `browser` | 浏览器指纹 | curl_cffi 浏览器指纹标识(如 chrome136)。 | `chrome136` | +| | `user_agent` | User-Agent | HTTP 请求的 User-Agent 字符串。 | `Mozilla/5.0 (Macintosh; ...)` | +| **chat** | `temporary` | 临时对话 | 是否启用临时对话模式。 | `true` | +| | `disable_memory` | 禁用记忆 | 禁用 Grok 记忆功能,防止响应中出现不相关上下文。 | `true` | | | `stream` | 流式响应 | 是否默认启用流式输出。 | `true` | | | `thinking` | 思维链 | 是否启用模型思维链输出。 | `true` | | | `dynamic_statsig` | 动态指纹 | 是否启用动态生成 Statsig 值。 | `true` | | | `filter_tags` | 过滤标签 | 自动过滤 Grok 响应中的特殊标签。 | `["xaiartifact", "xai:tool_usage_card", "grok:render"]` | -| | `timeout` | 超时时间 | 请求 Grok 服务的超时时间(秒)。 | `120` | -| | `base_proxy_url` | 基础代理 URL | 代理请求到 Grok 官网的基础服务地址。 | `""` | -| | `asset_proxy_url` | 资源代理 URL | 代理请求到 Grok 官网的静态资源(图片/视频)地址。 | `""` | -| | `cf_clearance` | CF Clearance | Cloudflare 验证 Cookie,用于验证 Cloudflare 的验证。 | `""` | -| | `max_retry` | 最大重试 | 请求 Grok 服务失败时的最大重试次数。 | `3` | +| **retry** | `max_retry` | 最大重试 | 请求 Grok 服务失败时的最大重试次数。 | `3` | | | `retry_status_codes` | 重试状态码 | 触发重试的 HTTP 状态码列表。 | `[401, 429, 403]` | | | `retry_backoff_base` | 退避基数 | 重试退避的基础延迟(秒)。 | `0.5` | | | `retry_backoff_factor` | 退避倍率 | 重试退避的指数放大系数。 | `2.0` | | | `retry_backoff_max` | 退避上限 | 单次重试等待的最大延迟(秒)。 | `30.0` | | | `retry_budget` | 退避预算 | 单次请求的最大重试总耗时(秒)。 | `90.0` | -| | `stream_idle_timeout` | 流空闲超时 | 流式响应空闲超时(秒),超过将断开。 | `45.0` | +| **timeout** | `stream_idle_timeout` | 流空闲超时 | 流式响应空闲超时(秒),超过将断开。 | `120.0` | | | `video_idle_timeout` | 视频空闲超时 | 视频生成空闲超时(秒),超过将断开。 | `90.0` | -| | `image_ws` | 图片 WS 生成 | 启用后 `/v1/images/generations` 走 WebSocket 直连。 | `false` | -| | `image_ws_blocked_seconds` | WS Blocked 阈值 | 收到中等图后超过该秒数仍无最终图则判定 blocked。 | `15` | -| | `image_ws_final_min_bytes` | WS 最终图最小字节 | 判定最终图的最小字节数(通常 JPG > 100KB)。 | `100000` | -| | `image_ws_nsfw` | WS NSFW | WS 请求里是否启用 NSFW。 | `true` | +| **image** | `image_ws` | WebSocket 生成 | 启用后 `/v1/images/generations` 走 WebSocket 直连。| `true` | +| | `image_ws_nsfw` | NSFW 模式 | WebSocket 请求是否启用 NSFW。 | `true` | +| | `image_ws_blocked_seconds` | Blocked 阈值 | 收到中等图后超过该秒数仍无最终图则判定 blocked。 | `15` | +| | `image_ws_final_min_bytes` | 最终图最小字节 | 判定最终图的最小字节数(通常 JPG > 100KB)。 | `100000` | +| | `image_ws_medium_min_bytes`| 中等图最小字节 | 判定中等质量图的最小字节数。 | `30000` | | **token** | `auto_refresh` | 自动刷新 | 是否开启 Token 自动刷新机制。 | `true` | -| | `refresh_interval_hours` | 刷新间隔 | Token 刷新的时间间隔(小时)。 | `8` | -| | `super_refresh_interval_hours` | Super 刷新间隔 | Super Token 刷新的时间间隔(小时)。 | `2` | +| | `refresh_interval_hours` | 刷新间隔 | 普通 Token 刷新的时间间隔(小时)。 | `8` | +| | `super_refresh_interval_hours` | Super 刷新间隔 | Super Token 刷新的时间间隔(小时)。 | `2` | | | `fail_threshold` | 失败阈值 | 单个 Token 连续失败多少次后被标记为不可用。 | `5` | | | `save_delay_ms` | 保存延迟 | Token 变更合并写入的延迟(毫秒)。 | `500` | -| | `reload_interval_sec` | 一致性刷新 | 多 worker 场景下 Token 状态刷新间隔(秒)。 | `30` | +| | `reload_interval_sec` | 同步间隔 | 多 worker 场景下 Token 状态刷新间隔(秒)。 | `30` | | **cache** | `enable_auto_clean` | 自动清理 | 是否启用缓存自动清理,开启后按上限自动回收。 | `true` | | | `limit_mb` | 清理阈值 | 缓存大小阈值(MB),超过阈值会触发清理。 | `1024` | -| **performance** | `nsfw_max_concurrent` | 开启 NSFW 模式并发上限 | 批量开启 NSFW 模式时的并发请求上限。推荐 10。 | `10` | -| | `nsfw_batch_size` | 开启 NSFW 模式批次大小 | 批量开启 NSFW 模式的单批处理数量。推荐 50。 | `50` | -| | `nsfw_max_tokens` | 开启 NSFW 模式最大数量 | 单次批量开启 NSFW 的 Token 数量上限,防止误操作。推荐 1000。 | `1000` | -| | `usage_max_concurrent` | Token 用量刷新并发上限 | 批量刷新 Token 用量时的并发请求上限。推荐 25。 | `25` | -| | `usage_batch_size` | Token 用量刷新批次大小 | 批量刷新 Token 用量的单批处理数量。推荐 50。 | `50` | -| | `usage_max_tokens` | Token 用量刷新最大数量 | 批量刷新 Token 用量的单次处理数量上限。推荐 1000。 | `1000` | -| | `assets_max_concurrent` | 在线资产查找/删除并发上限 | 在线资产查找/删除时的并发请求上限。推荐 25。 | `25` | -| | `assets_batch_size` | 在线资产查找/删除批次大小 | 在线资产查找/删除的单批处理数量。推荐 10。 | `10` | -| | `assets_max_tokens` | 在线资产查找/删除最大数量 | 在线资产查找/删除的单次处理数量上限。推荐 1000。 | `1000` | -| | `assets_delete_batch_size` | 在线资产删除批量 | 在线资产删除单批并发数量。推荐 10。 | `10` | -| | `media_max_concurrent` | 媒体并发上限 | 视频/媒体生成请求的并发上限。推荐 50。 | `50` | +| **performance** | `media_max_concurrent` | Media 并发上限 | 视频/媒体生成请求的并发上限。推荐 50。 | `50` | +| | `assets_max_concurrent` | Assets 并发上限 | 批量查找/删除资产时的并发请求上限。推荐 25。 | `25` | +| | `assets_batch_size` | Assets 批次大小 | 批量查找/删除资产时的单批处理数量。推荐 10。 | `10` | +| | `assets_max_tokens` | Assets 最大数量 | 单次批量查找/删除资产时的处理数量上限。推荐 1000。 | `1000` | +| | `assets_delete_batch_size` | Assets 删除批量 | 单账号批量删除资产时的单批并发数量。推荐 10。 | `10` | +| | `usage_max_concurrent` | Token 刷新并发上限 | 批量刷新 Token 用量时的并发请求上限。推荐 25。 | `25` | +| | `usage_batch_size` | Token 刷新批次大小 | 批量刷新 Token 用量的单批处理数量。推荐 50。 | `50` | +| | `usage_max_tokens` | Token 刷新最大数量 | 单次批量刷新 Token 用量时的处理数量上限。推荐 1000。| `1000` | +| | `nsfw_max_concurrent` | NSFW 开启并发上限 | 批量开启 NSFW 模式时的并发请求上限。推荐 10。 | `10` | +| | `nsfw_batch_size` | NSFW 开启批次大小 | 批量开启 NSFW 模式的单批处理数量。推荐 50。 | `50` | +| | `nsfw_max_tokens` | NSFW 开启最大数量 | 单次批量开启 NSFW 的 Token 数量上限。推荐 1000。 | `1000` |