Skip to content

Commit 6f0c1de

Browse files
fix: align image generation with current Grok website behavior
- Accept all cardAttachment image types: render_generated_image, render_edited_image, AND render_searched_image (Grok now returns searched images when it decides not to generate) - Remove ws_imagine (WebSocket) fallback — deprecated endpoint, always returns 401 - Fix loguru format strings (%s → {}) for proper error logging - Add on-demand CF clearance refresh on 403 with 60s debounce
1 parent 959a114 commit 6f0c1de

3 files changed

Lines changed: 69 additions & 63 deletions

File tree

app/services/grok/services/image.py

Lines changed: 19 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -107,36 +107,16 @@ async def _stream_retry() -> AsyncGenerator[str, None]:
107107
tried_tokens.add(current_token)
108108
yielded = False
109109
try:
110-
try:
111-
result = await self._stream_app_chat(
112-
token_mgr=token_mgr,
113-
token=current_token,
114-
model_info=model_info,
115-
prompt=prompt,
116-
n=n,
117-
response_format=response_format,
118-
enable_nsfw=enable_nsfw,
119-
chat_format=chat_format,
120-
)
121-
except UpstreamException as app_chat_error:
122-
if rate_limited(app_chat_error):
123-
raise
124-
logger.warning(
125-
"App-chat image stream failed, falling back to ws_imagine: %s",
126-
app_chat_error,
127-
)
128-
result = await self._stream_ws(
129-
token_mgr=token_mgr,
130-
token=current_token,
131-
model_info=model_info,
132-
prompt=prompt,
133-
n=n,
134-
response_format=response_format,
135-
size=size,
136-
aspect_ratio=aspect_ratio,
137-
enable_nsfw=enable_nsfw,
138-
chat_format=chat_format,
139-
)
110+
result = await self._stream_app_chat(
111+
token_mgr=token_mgr,
112+
token=current_token,
113+
model_info=model_info,
114+
prompt=prompt,
115+
n=n,
116+
response_format=response_format,
117+
enable_nsfw=enable_nsfw,
118+
chat_format=chat_format,
119+
)
140120
async for chunk in result.data:
141121
yielded = True
142122
yield chunk
@@ -186,34 +166,15 @@ async def _stream_retry() -> AsyncGenerator[str, None]:
186166

187167
tried_tokens.add(current_token)
188168
try:
189-
try:
190-
return await self._collect_app_chat(
191-
token_mgr=token_mgr,
192-
token=current_token,
193-
model_info=model_info,
194-
prompt=prompt,
195-
n=n,
196-
response_format=response_format,
197-
enable_nsfw=enable_nsfw,
198-
)
199-
except UpstreamException as app_chat_error:
200-
if rate_limited(app_chat_error):
201-
raise
202-
logger.warning(
203-
"App-chat image collect failed, falling back to ws_imagine: %s",
204-
app_chat_error,
205-
)
206-
return await self._collect_ws(
207-
token_mgr=token_mgr,
208-
token=current_token,
209-
model_info=model_info,
210-
tried_tokens=tried_tokens,
211-
prompt=prompt,
212-
n=n,
213-
response_format=response_format,
214-
aspect_ratio=aspect_ratio,
215-
enable_nsfw=enable_nsfw,
216-
)
169+
return await self._collect_app_chat(
170+
token_mgr=token_mgr,
171+
token=current_token,
172+
model_info=model_info,
173+
prompt=prompt,
174+
n=n,
175+
response_format=response_format,
176+
enable_nsfw=enable_nsfw,
177+
)
217178
except UpstreamException as e:
218179
last_error = e
219180
if rate_limited(e):

app/services/grok/services/image_edit.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,16 @@ async def process(
348348
if ca := resp.get("cardAttachment"):
349349
try:
350350
jd = orjson.loads(ca.get("jsonData", b"{}"))
351-
if jd.get("type") in ("render_generated_image", "render_edited_image"):
351+
card_type = jd.get("type", "")
352+
url = None
353+
if card_type in ("render_generated_image", "render_edited_image"):
352354
chunk = jd.get("image_chunk", {})
353355
if chunk.get("progress", 0) >= 100 and chunk.get("imageUrl"):
354356
url = f"https://assets.grok.com/{chunk['imageUrl']}"
357+
elif card_type == "render_searched_image":
358+
img = jd.get("image", {})
359+
url = img.get("original") or img.get("thumbnail")
360+
if url:
355361
if self.response_format == "url":
356362
processed = await self.process_url(url, "image")
357363
if processed:
@@ -536,10 +542,16 @@ async def process(self, response: AsyncIterable[bytes]) -> List[str]:
536542
if ca := resp.get("cardAttachment"):
537543
try:
538544
jd = orjson.loads(ca.get("jsonData", b"{}"))
539-
if jd.get("type") in ("render_generated_image", "render_edited_image"):
545+
card_type = jd.get("type", "")
546+
url = None
547+
if card_type in ("render_generated_image", "render_edited_image"):
540548
chunk = jd.get("image_chunk", {})
541549
if chunk.get("progress", 0) >= 100 and chunk.get("imageUrl"):
542550
url = f"https://assets.grok.com/{chunk['imageUrl']}"
551+
elif card_type == "render_searched_image":
552+
img = jd.get("image", {})
553+
url = img.get("original") or img.get("thumbnail")
554+
if url:
543555
if self.response_format == "url":
544556
processed = await self.process_url(url, "image")
545557
if processed:
@@ -558,8 +570,8 @@ async def process(self, response: AsyncIterable[bytes]) -> List[str]:
558570
processed = await self.process_url(url, "image")
559571
if processed:
560572
images.append(processed)
561-
except Exception:
562-
pass
573+
except Exception as card_err:
574+
logger.warning(f"cardAttachment processing error: {card_err}")
563575

564576
if mr := resp.get("modelResponse"):
565577
if urls := _collect_images(mr):

app/services/reverse/app_chat.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
Reverse interface: app chat conversations.
33
"""
44

5+
import asyncio
56
import inspect
7+
import time
68
import orjson
79
from typing import Any, Dict, List, Optional
810
from urllib.parse import urlparse
@@ -19,6 +21,35 @@
1921
CHAT_API = "https://grok.com/rest/app-chat/conversations/new"
2022
_LAST_PROXY_LOG_STATE: tuple[str, str] | None = None
2123

24+
# CF 刷新防抖:60 秒内不重复刷新
25+
_last_cf_refresh_time: float = 0.0
26+
_cf_refresh_lock = asyncio.Lock()
27+
_CF_REFRESH_COOLDOWN = 60.0
28+
29+
30+
async def _trigger_cf_refresh_on_403() -> bool:
31+
"""403 时触发一次 CF clearance 刷新(带防抖)"""
32+
global _last_cf_refresh_time
33+
now = time.monotonic()
34+
if now - _last_cf_refresh_time < _CF_REFRESH_COOLDOWN:
35+
logger.debug("CF refresh skipped (cooldown)")
36+
return False
37+
async with _cf_refresh_lock:
38+
# 双重检查
39+
now = time.monotonic()
40+
if now - _last_cf_refresh_time < _CF_REFRESH_COOLDOWN:
41+
return False
42+
logger.info("403 detected, triggering on-demand CF refresh...")
43+
try:
44+
from app.services.cf_refresh.scheduler import refresh_once
45+
success = await refresh_once()
46+
_last_cf_refresh_time = time.monotonic()
47+
return success
48+
except Exception as e:
49+
logger.error("On-demand CF refresh failed: {}", e)
50+
_last_cf_refresh_time = time.monotonic()
51+
return False
52+
2253

2354
def _normalize_chat_proxy(proxy_url: str) -> str:
2455
"""Normalize proxy URL for curl-cffi app-chat requests."""
@@ -291,7 +322,7 @@ async def _do_request():
291322
content_type = str(response.headers.get("content-type", ""))
292323

293324
logger.error(
294-
"AppChatReverse: Chat failed, %s, content_type=%s, body=%s",
325+
"AppChatReverse: Chat failed, {}, content_type={}, body={}",
295326
response.status_code,
296327
content_type,
297328
content[:500],
@@ -313,6 +344,8 @@ def extract_status(e: Exception) -> Optional[int]:
313344
async def _on_retry(attempt: int, status_code: int, error: Exception, delay: float):
314345
if active_proxy_key and should_rotate_proxy(status_code):
315346
rotate_proxy(active_proxy_key)
347+
if status_code == 403:
348+
await _trigger_cf_refresh_on_403()
316349

317350
response = await retry_on_status(
318351
_do_request,

0 commit comments

Comments
 (0)