From 0d73504f1a26b7a209e4cc03074db8cb9c49f8e0 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:58:42 +0800 Subject: [PATCH 1/2] feat: add ResettableSession for managing async HTTP sessions with automatic reset on specified status codes --- .gitignore | 7 +- app/services/grok/batch_services/assets.py | 9 +-- app/services/grok/batch_services/nsfw.py | 5 +- app/services/grok/batch_services/usage.py | 5 +- app/services/grok/services/chat.py | 4 +- app/services/grok/services/video.py | 12 +-- app/services/grok/services/voice.py | 5 +- app/services/grok/utils/download.py | 8 +- app/services/grok/utils/upload.py | 8 +- app/services/reverse/utils/retry.py | 10 ++- app/services/reverse/utils/session.py | 87 ++++++++++++++++++++++ app/static/admin/pages/cache.html | 14 ++-- app/static/admin/pages/config.html | 12 +-- app/static/admin/pages/login.html | 10 +-- app/static/admin/pages/token.html | 14 ++-- app/static/common/js/footer.js | 2 +- app/static/common/js/header.js | 2 +- app/static/common/js/public-header.js | 2 +- app/static/public/css/imagine.css | 31 ++++++++ app/static/public/js/imagine.js | 38 +++++++++- app/static/public/pages/imagine.html | 12 +-- app/static/public/pages/login.html | 10 +-- app/static/public/pages/video.html | 12 +-- app/static/public/pages/voice.html | 12 +-- config.defaults.toml | 2 + data/config.toml | 84 --------------------- pyproject.toml | 2 +- 27 files changed, 241 insertions(+), 178 deletions(-) create mode 100644 app/services/reverse/utils/session.py delete mode 100644 data/config.toml diff --git a/.gitignore b/.gitignore index 7b897ce0..d142e851 100644 --- a/.gitignore +++ b/.gitignore @@ -45,12 +45,9 @@ logs/ *.log # Data -data/*.json -data/tmp/* -data/.locks/* -app/data/*.json app/data/tmp/* -app/data/.locks/* +data/ +app/data/ # Testing .pytest_cache/ diff --git a/app/services/grok/batch_services/assets.py b/app/services/grok/batch_services/assets.py index 7c3c31e3..1ddd975f 100644 --- a/app/services/grok/batch_services/assets.py +++ b/app/services/grok/batch_services/assets.py @@ -5,12 +5,11 @@ import asyncio from typing import Dict, List, Optional -from curl_cffi.requests import AsyncSession - from app.core.config import get_config from app.core.logger import logger from app.services.reverse.assets_list import AssetsListReverse from app.services.reverse.assets_delete import AssetsDeleteReverse +from app.services.reverse.utils.session import ResettableSession from app.core.batch import run_batch @@ -18,11 +17,11 @@ class BaseAssetsService: """Base assets service.""" def __init__(self): - self._session: Optional[AsyncSession] = None + self._session: Optional[ResettableSession] = None - async def _get_session(self) -> AsyncSession: + async def _get_session(self) -> ResettableSession: if self._session is None: - self._session = AsyncSession() + self._session = ResettableSession() return self._session async def close(self): diff --git a/app/services/grok/batch_services/nsfw.py b/app/services/grok/batch_services/nsfw.py index 1c8faa0c..f9dca668 100644 --- a/app/services/grok/batch_services/nsfw.py +++ b/app/services/grok/batch_services/nsfw.py @@ -5,14 +5,13 @@ import asyncio from typing import Callable, Awaitable, Dict, Any, Optional -from curl_cffi.requests import AsyncSession - from app.core.logger import logger from app.core.config import get_config from app.core.exceptions import UpstreamException from app.services.reverse.accept_tos import AcceptTosReverse from app.services.reverse.nsfw_mgmt import NsfwMgmtReverse from app.services.reverse.set_birth import SetBirthReverse +from app.services.reverse.utils.session import ResettableSession from app.core.batch import run_batch @@ -44,7 +43,7 @@ async def batch( async def _enable(token: str): try: browser = get_config("proxy.browser") - async with AsyncSession(impersonate=browser) as session: + async with ResettableSession(impersonate=browser) as session: async def _record_fail(err: UpstreamException, reason: str): status = None if err.details and "status" in err.details: diff --git a/app/services/grok/batch_services/usage.py b/app/services/grok/batch_services/usage.py index de40117b..fa0630b7 100644 --- a/app/services/grok/batch_services/usage.py +++ b/app/services/grok/batch_services/usage.py @@ -5,11 +5,10 @@ import asyncio from typing import Callable, Awaitable, Dict, Any, Optional, List -from curl_cffi.requests import AsyncSession - from app.core.logger import logger from app.core.config import get_config from app.services.reverse.rate_limits import RateLimitsReverse +from app.services.reverse.utils.session import ResettableSession from app.core.batch import run_batch _USAGE_SEMAPHORE = None @@ -43,7 +42,7 @@ async def get(self, token: str) -> Dict: """ async with _get_usage_semaphore(): try: - async with AsyncSession() as session: + async with ResettableSession() as session: response = await RateLimitsReverse.request(session, token) data = response.json() remaining = data.get("remainingTokens") diff --git a/app/services/grok/services/chat.py b/app/services/grok/services/chat.py index 19853a82..3f1cb5ae 100644 --- a/app/services/grok/services/chat.py +++ b/app/services/grok/services/chat.py @@ -8,7 +8,6 @@ from typing import Dict, List, Any, AsyncGenerator, AsyncIterable import orjson -from curl_cffi.requests import AsyncSession from curl_cffi.requests.errors import RequestsError from app.core.logger import logger @@ -25,6 +24,7 @@ from app.services.grok.utils import process as proc_base from app.services.grok.utils.retry import pick_token, rate_limited from app.services.reverse.app_chat import AppChatReverse +from app.services.reverse.utils.session import ResettableSession from app.services.grok.utils.stream import wrap_stream_with_usage from app.services.token import get_token_manager, EffortType @@ -190,7 +190,7 @@ async def chat( browser = get_config("proxy.browser") async def _stream(): - session = AsyncSession(impersonate=browser) + session = ResettableSession(impersonate=browser) try: async with _get_chat_semaphore(): stream_response = await AppChatReverse.request( diff --git a/app/services/grok/services/video.py b/app/services/grok/services/video.py index 70f477e3..d25d19c1 100644 --- a/app/services/grok/services/video.py +++ b/app/services/grok/services/video.py @@ -8,7 +8,6 @@ from typing import Any, AsyncGenerator, AsyncIterable, Optional import orjson -from curl_cffi.requests import AsyncSession from curl_cffi.requests.errors import RequestsError from app.core.logger import logger @@ -33,6 +32,7 @@ from app.services.reverse.app_chat import AppChatReverse from app.services.reverse.media_post import MediaPostReverse from app.services.reverse.video_upscale import VideoUpscaleReverse +from app.services.reverse.utils.session import ResettableSession from app.services.token.manager import BASIC_POOL_NAME _VIDEO_SEMAPHORE = None @@ -69,7 +69,7 @@ async def create_post( prompt_value = prompt if media_type == "MEDIA_POST_TYPE_VIDEO" else "" media_value = media_url or "" - async with AsyncSession() as session: + async with ResettableSession() as session: async with _get_video_semaphore(): response = await MediaPostReverse.request( session, @@ -131,7 +131,7 @@ async def generate( } async def _stream(): - session = AsyncSession() + session = ResettableSession() try: async with _get_video_semaphore(): stream_response = await AppChatReverse.request( @@ -191,7 +191,7 @@ async def generate_from_image( } async def _stream(): - session = AsyncSession() + session = ResettableSession() try: async with _get_video_semaphore(): stream_response = await AppChatReverse.request( @@ -401,7 +401,7 @@ async def _upscale_video_url(self, video_url: str) -> str: logger.warning("Video upscale skipped: unable to extract video id") return video_url try: - async with AsyncSession() as session: + async with ResettableSession() as session: response = await VideoUpscaleReverse.request( session, self.token, video_id ) @@ -583,7 +583,7 @@ async def _upscale_video_url(self, video_url: str) -> str: logger.warning("Video upscale skipped: unable to extract video id") return video_url try: - async with AsyncSession() as session: + async with ResettableSession() as session: response = await VideoUpscaleReverse.request( session, self.token, video_id ) diff --git a/app/services/grok/services/voice.py b/app/services/grok/services/voice.py index b72fce3e..1a08e016 100644 --- a/app/services/grok/services/voice.py +++ b/app/services/grok/services/voice.py @@ -4,10 +4,9 @@ from typing import Any, Dict -from curl_cffi.requests import AsyncSession - from app.core.config import get_config from app.services.reverse.ws_livekit import LivekitTokenReverse +from app.services.reverse.utils.session import ResettableSession class VoiceService: @@ -21,7 +20,7 @@ async def get_token( speed: float = 1.0, ) -> Dict[str, Any]: browser = get_config("proxy.browser") - async with AsyncSession(impersonate=browser) as session: + async with ResettableSession(impersonate=browser) as session: response = await LivekitTokenReverse.request( session, token=token, diff --git a/app/services/grok/utils/download.py b/app/services/grok/utils/download.py index ddb04489..bac0feae 100644 --- a/app/services/grok/utils/download.py +++ b/app/services/grok/utils/download.py @@ -13,13 +13,13 @@ from urllib.parse import urlparse import aiofiles -from curl_cffi.requests import AsyncSession from app.core.logger import logger from app.core.storage import DATA_DIR from app.core.config import get_config from app.core.exceptions import AppException from app.services.reverse.assets_download import AssetsDownloadReverse +from app.services.reverse.utils.session import ResettableSession from app.services.grok.utils.locks import _get_download_semaphore, _file_lock @@ -27,7 +27,7 @@ class DownloadService: """Assets download service.""" def __init__(self): - self._session: Optional[AsyncSession] = None + self._session: Optional[ResettableSession] = None base_dir = DATA_DIR / "tmp" self.image_dir = base_dir / "image" self.video_dir = base_dir / "video" @@ -35,10 +35,10 @@ def __init__(self): self.video_dir.mkdir(parents=True, exist_ok=True) self._cleanup_running = False - async def create(self) -> AsyncSession: + async def create(self) -> ResettableSession: """Create or reuse a session.""" if self._session is None: - self._session = AsyncSession() + self._session = ResettableSession() return self._session async def close(self): diff --git a/app/services/grok/utils/upload.py b/app/services/grok/utils/upload.py index ff5cb7ee..c70b7964 100644 --- a/app/services/grok/utils/upload.py +++ b/app/services/grok/utils/upload.py @@ -13,13 +13,13 @@ from urllib.parse import urlparse import aiofiles -from curl_cffi.requests import AsyncSession from app.core.config import get_config from app.core.exceptions import AppException, UpstreamException, ValidationException from app.core.logger import logger from app.core.storage import DATA_DIR from app.services.reverse.assets_upload import AssetsUploadReverse +from app.services.reverse.utils.session import ResettableSession from app.services.grok.utils.locks import _get_upload_semaphore, _file_lock @@ -27,13 +27,13 @@ class UploadService: """Assets upload service.""" def __init__(self): - self._session: Optional[AsyncSession] = None + self._session: Optional[ResettableSession] = None self._chunk_size = 64 * 1024 - async def create(self) -> AsyncSession: + async def create(self) -> ResettableSession: """Create or reuse a session.""" if self._session is None: - self._session = AsyncSession() + self._session = ResettableSession() return self._session async def close(self): diff --git a/app/services/reverse/utils/retry.py b/app/services/reverse/utils/retry.py index 0de15b6f..971eab05 100644 --- a/app/services/reverse/utils/retry.py +++ b/app/services/reverse/utils/retry.py @@ -3,6 +3,7 @@ """ import asyncio +import inspect import random from typing import Callable, Any, Optional @@ -122,7 +123,7 @@ async def retry_on_status( func: Callable, *args, extract_status: Callable[[Exception], Optional[int]] = None, - on_retry: Callable[[int, int, Exception, float], None] = None, + on_retry: Callable[[int, int, Exception, float], Any] = None, **kwargs, ) -> Any: """ @@ -132,7 +133,8 @@ async def retry_on_status( func: Retry function *args: Function arguments extract_status: Function to extract status code from exception - on_retry: Callback function for retry (attempt, status_code, error, delay) + on_retry: Callback function for retry (attempt, status_code, error, delay). + Can be sync or async. **kwargs: Function keyword arguments Returns: @@ -204,7 +206,9 @@ def extract_status(e: Exception) -> Optional[int]: # Callback if on_retry: - on_retry(ctx.attempt, status_code, e, delay) + result = on_retry(ctx.attempt, status_code, e, delay) + if inspect.isawaitable(result): + await result await asyncio.sleep(delay) continue diff --git a/app/services/reverse/utils/session.py b/app/services/reverse/utils/session.py new file mode 100644 index 00000000..602bd795 --- /dev/null +++ b/app/services/reverse/utils/session.py @@ -0,0 +1,87 @@ +""" +Resettable session wrapper for reverse requests. +""" + +import asyncio +from typing import Any, Iterable, Optional + +from curl_cffi.requests import AsyncSession + +from app.core.config import get_config +from app.core.logger import logger + + +class ResettableSession: + """AsyncSession wrapper that resets connection on specific HTTP status codes.""" + + def __init__( + self, + *, + reset_on_status: Optional[Iterable[int]] = None, + **session_kwargs: Any, + ): + self._session_kwargs = dict(session_kwargs) + config_codes = get_config("retry.reset_session_status_codes") + if reset_on_status is None: + reset_on_status = config_codes if config_codes is not None else [403] + if isinstance(reset_on_status, int): + reset_on_status = [reset_on_status] + self._reset_on_status = ( + {int(code) for code in reset_on_status} if reset_on_status else set() + ) + self._reset_requested = False + self._reset_lock = asyncio.Lock() + self._session = AsyncSession(**self._session_kwargs) + + async def _maybe_reset(self) -> None: + if not self._reset_requested: + return + async with self._reset_lock: + if not self._reset_requested: + return + self._reset_requested = False + old_session = self._session + self._session = AsyncSession(**self._session_kwargs) + try: + await old_session.close() + except Exception: + pass + logger.debug("ResettableSession: session reset") + + async def _request(self, method: str, *args: Any, **kwargs: Any): + await self._maybe_reset() + response = await getattr(self._session, method)(*args, **kwargs) + if self._reset_on_status and response.status_code in self._reset_on_status: + self._reset_requested = True + return response + + async def get(self, *args: Any, **kwargs: Any): + return await self._request("get", *args, **kwargs) + + async def post(self, *args: Any, **kwargs: Any): + return await self._request("post", *args, **kwargs) + + async def reset(self) -> None: + self._reset_requested = True + await self._maybe_reset() + + async def close(self) -> None: + if self._session is None: + return + try: + await self._session.close() + finally: + self._session = None + self._reset_requested = False + + async def __aenter__(self) -> "ResettableSession": + return self + + async def __aexit__(self, exc_type, exc, tb) -> None: + await self.close() + + def __getattr__(self, name: str) -> Any: + return getattr(self._session, name) + + +__all__ = ["ResettableSession"] diff --git a/app/static/admin/pages/cache.html b/app/static/admin/pages/cache.html index 52055d24..e5970c4d 100644 --- a/app/static/admin/pages/cache.html +++ b/app/static/admin/pages/cache.html @@ -9,8 +9,8 @@ - - + + @@ -196,12 +196,12 @@

缓存管理

- - - - + + + + - + diff --git a/app/static/admin/pages/config.html b/app/static/admin/pages/config.html index 68786b0d..ab293b0a 100644 --- a/app/static/admin/pages/config.html +++ b/app/static/admin/pages/config.html @@ -9,8 +9,8 @@ - - + + @@ -46,11 +46,11 @@

配置管理

- - - + + + - + diff --git a/app/static/admin/pages/login.html b/app/static/admin/pages/login.html index 913c33f7..9066c8ce 100644 --- a/app/static/admin/pages/login.html +++ b/app/static/admin/pages/login.html @@ -24,8 +24,8 @@ } } - - + + @@ -55,10 +55,10 @@ - + - - + + diff --git a/app/static/admin/pages/token.html b/app/static/admin/pages/token.html index daf49759..4ef7bfad 100644 --- a/app/static/admin/pages/token.html +++ b/app/static/admin/pages/token.html @@ -9,8 +9,8 @@ - - + + @@ -292,12 +292,12 @@ - - - - + + + + - + diff --git a/app/static/common/js/footer.js b/app/static/common/js/footer.js index e7925d93..03a01db1 100644 --- a/app/static/common/js/footer.js +++ b/app/static/common/js/footer.js @@ -2,7 +2,7 @@ async function loadAdminFooter() { const container = document.getElementById('app-footer'); if (!container) return; try { - const res = await fetch('/static/common/html/footer.html?v=0.3.0'); + const res = await fetch('/static/common/html/footer.html?v=0.3.1'); if (!res.ok) return; container.innerHTML = await res.text(); } catch (e) { diff --git a/app/static/common/js/header.js b/app/static/common/js/header.js index 27cdae1e..148496fb 100644 --- a/app/static/common/js/header.js +++ b/app/static/common/js/header.js @@ -2,7 +2,7 @@ async function loadAdminHeader() { const container = document.getElementById('app-header'); if (!container) return; try { - const res = await fetch('/static/common/html/header.html?v=0.3.0'); + const res = await fetch('/static/common/html/header.html?v=0.3.1'); if (!res.ok) return; container.innerHTML = await res.text(); const path = window.location.pathname; diff --git a/app/static/common/js/public-header.js b/app/static/common/js/public-header.js index 35459bdf..2c1f82d5 100644 --- a/app/static/common/js/public-header.js +++ b/app/static/common/js/public-header.js @@ -2,7 +2,7 @@ async function loadPublicHeader() { const container = document.getElementById('app-header'); if (!container) return; try { - const res = await fetch('/static/common/html/public-header.html?v=0.3.0'); + const res = await fetch('/static/common/html/public-header.html?v=0.3.1'); if (!res.ok) return; container.innerHTML = await res.text(); const logoutBtn = container.querySelector('#public-logout-btn'); diff --git a/app/static/public/css/imagine.css b/app/static/public/css/imagine.css index 5a15513b..19080cff 100644 --- a/app/static/public/css/imagine.css +++ b/app/static/public/css/imagine.css @@ -691,6 +691,37 @@ border-radius: 0 0 14px 14px; } +.waterfall-meta .meta-right { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.image-status { + font-size: 10px; + font-weight: 600; + padding: 2px 6px; + border-radius: 999px; + background: #f3f4f6; + color: var(--accents-5); + line-height: 1; +} + +.image-status.running { + background: #fef3c7; + color: #b45309; +} + +.image-status.done { + background: #d1fae5; + color: #047857; +} + +.image-status.error { + background: #fee2e2; + color: #b91c1c; +} + .waterfall-meta span { font-family: 'Geist Mono', ui-monospace, monospace; color: var(--accents-5); diff --git a/app/static/public/js/imagine.js b/app/static/public/js/imagine.js index cb9234ca..1ddf2877 100644 --- a/app/static/public/js/imagine.js +++ b/app/static/public/js/imagine.js @@ -51,7 +51,7 @@ function setStatus(state, text) { if (!statusText) return; - statusText.textContent = text; + statusText.textContent = text || '未连接'; statusText.classList.remove('connected', 'connecting', 'error'); if (state) { statusText.classList.add(state); @@ -144,6 +144,17 @@ function updateError(value) {} + function setImageStatus(item, state, label) { + if (!item) return; + const statusEl = item.querySelector('.image-status'); + if (!statusEl) return; + statusEl.textContent = label; + statusEl.classList.remove('running', 'done', 'error'); + if (state) { + statusEl.classList.add(state); + } + } + function isLikelyBase64(raw) { if (!raw) return false; if (raw.startsWith('data:')) return true; @@ -325,6 +336,11 @@ metaBar.className = 'waterfall-meta'; const left = document.createElement('div'); left.textContent = meta && meta.sequence ? `#${meta.sequence}` : '#'; + const rightWrap = document.createElement('div'); + rightWrap.className = 'meta-right'; + const status = document.createElement('span'); + status.className = 'image-status done'; + status.textContent = '完成'; const right = document.createElement('span'); if (meta && meta.elapsed_ms) { right.textContent = `${meta.elapsed_ms}ms`; @@ -332,8 +348,10 @@ right.textContent = ''; } + rightWrap.appendChild(status); + rightWrap.appendChild(right); metaBar.appendChild(left); - metaBar.appendChild(right); + metaBar.appendChild(rightWrap); item.appendChild(checkbox); item.appendChild(img); @@ -432,14 +450,21 @@ metaBar.className = 'waterfall-meta'; const left = document.createElement('div'); left.textContent = `#${sequence}`; + const rightWrap = document.createElement('div'); + rightWrap.className = 'meta-right'; + const status = document.createElement('span'); + status.className = `image-status ${isFinal ? 'done' : 'running'}`; + status.textContent = isFinal ? '完成' : '生成中'; const right = document.createElement('span'); right.textContent = ''; if (meta && meta.elapsed_ms) { right.textContent = `${meta.elapsed_ms}ms`; } + rightWrap.appendChild(status); + rightWrap.appendChild(right); metaBar.appendChild(left); - metaBar.appendChild(right); + metaBar.appendChild(rightWrap); item.appendChild(checkbox); item.appendChild(img); @@ -471,12 +496,13 @@ img.src = dataUrl; } item.dataset.imageUrl = dataUrl; - const right = item.querySelector('.waterfall-meta span'); + const right = item.querySelector('.waterfall-meta .meta-right span:last-child'); if (right && meta && meta.elapsed_ms) { right.textContent = `${meta.elapsed_ms}ms`; } } + setImageStatus(item, isFinal ? 'done' : 'running', isFinal ? '完成' : '生成中'); updateError(''); if (isNew && autoScrollToggle && autoScrollToggle.checked) { @@ -537,6 +563,10 @@ } } else if (data.type === 'error' || data.error) { const message = data.message || (data.error && data.error.message) || '生成失败'; + const errorImageId = data.image_id || data.imageId; + if (errorImageId && streamImageMap.has(errorImageId)) { + setImageStatus(streamImageMap.get(errorImageId), 'error', '失败'); + } updateError(message); toast(message, 'error'); } diff --git a/app/static/public/pages/imagine.html b/app/static/public/pages/imagine.html index 861d3c58..7a09c9cc 100644 --- a/app/static/public/pages/imagine.html +++ b/app/static/public/pages/imagine.html @@ -9,8 +9,8 @@ - - + + @@ -237,12 +237,12 @@

Imagine 瀑布流

- - - + + + - + diff --git a/app/static/public/pages/login.html b/app/static/public/pages/login.html index d3a29482..d72aa889 100644 --- a/app/static/public/pages/login.html +++ b/app/static/public/pages/login.html @@ -24,8 +24,8 @@ } } - - + + @@ -59,10 +59,10 @@ - + - - + + diff --git a/app/static/public/pages/video.html b/app/static/public/pages/video.html index 010fd77b..5f779080 100644 --- a/app/static/public/pages/video.html +++ b/app/static/public/pages/video.html @@ -9,8 +9,8 @@ - - + + @@ -158,11 +158,11 @@

Video 视频生成

- - - + + + - + diff --git a/app/static/public/pages/voice.html b/app/static/public/pages/voice.html index e9fe5cca..802843cc 100644 --- a/app/static/public/pages/voice.html +++ b/app/static/public/pages/voice.html @@ -9,8 +9,8 @@ - - + + @@ -147,11 +147,11 @@

LiveKit 陪聊

- - - + + + - + diff --git a/config.defaults.toml b/config.defaults.toml index 24bce1f0..45ba2066 100644 --- a/config.defaults.toml +++ b/config.defaults.toml @@ -48,6 +48,8 @@ user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 max_retry = 3 # 触发重试的 HTTP 状态码 retry_status_codes = [401,429,403] +# 触发重建 session 的 HTTP 状态码(用于轮换代理) +reset_session_status_codes = [403] # 退避基础延迟(秒) retry_backoff_base = 0.5 # 退避倍率 diff --git a/data/config.toml b/data/config.toml deleted file mode 100644 index 7ad6fadc..00000000 --- a/data/config.toml +++ /dev/null @@ -1,84 +0,0 @@ -[app] -app_url = "http://127.0.0.1:8000" -app_key = "grok2api" -api_key = "" -public_enabled = true -public_key = "" -image_format = "url" -video_format = "html" -temporary = true -disable_memory = true -stream = true -thinking = true -dynamic_statsig = true -filter_tags = ["xaiartifact","xai:tool_usage_card","grok:render"] - -[proxy] -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" - -[retry] -max_retry = 3 -retry_status_codes = [401,429,403] -retry_backoff_base = 0.5 -retry_backoff_factor = 2 -retry_backoff_max = 30 -retry_budget = 90 - -[token] -auto_refresh = true -refresh_interval_hours = 8 -super_refresh_interval_hours = 2 -fail_threshold = 5 -save_delay_ms = 200 -reload_interval_sec = 30 - -[cache] -enable_auto_clean = true -limit_mb = 1024 - -[chat] -concurrent = 10 -timeout = 60 -stream_timeout = 60 - -[image] -timeout = 120 -stream_timeout = 120 -final_timeout = 15 -nsfw = true -medium_min_bytes = 30000 -final_min_bytes = 100000 - -[video] -concurrent = 10 -timeout = 60 -stream_timeout = 60 - -[voice] -timeout = 120 - -[asset] -upload_concurrent = 30 -upload_timeout = 60 -download_concurrent = 30 -download_timeout = 60 -list_concurrent = 10 -list_timeout = 60 -list_batch_size = 10 -delete_concurrent = 10 -delete_timeout = 60 -delete_batch_size = 10 - -[nsfw] -concurrent = 10 -batch_size = 50 -timeout = 60 - -[usage] -concurrent = 10 -batch_size = 50 -timeout = 60 diff --git a/pyproject.toml b/pyproject.toml index f808a049..92fd7316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "grok2api" -version = "0.3.0" +version = "0.3.1" description = "Grok2API rebuilt with FastAPI, fully aligned with the latest web call format. Supports streaming and non-streaming chat, image generation/editing, deep thinking, token pool concurrency, and automatic load balancing." readme = "README.md" requires-python = ">=3.13" From 716eb08bce17c4ae1db432250d63ecd2f8703a32 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:58:53 +0800 Subject: [PATCH 2/2] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index d142e851..75821d5b 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,6 @@ logs/ *.log # Data -app/data/tmp/* data/ app/data/