Files
grok2api/app/services/reverse/assets_upload.py
T

304 lines
13 KiB
Python

"""
Reverse interface: upload asset.
"""
import asyncio
import json
import urllib.error
import urllib.request
from dataclasses import dataclass
from typing import Any
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.token.service import TokenService
from app.services.reverse.utils.headers import build_headers
from app.services.reverse.utils.retry import retry_on_status
UPLOAD_API = "https://grok.com/rest/app-chat/upload-file"
class AssetsUploadReverse:
"""/rest/app-chat/upload-file reverse interface."""
@dataclass
class _SimpleResponse:
status_code: int
headers: dict[str, str]
text: str
def json(self):
return json.loads(self.text or "{}")
@staticmethod
async def _urllib_post(
url: str, headers: dict[str, str], payload: dict[str, Any], timeout: int, proxy_url: str
) -> "AssetsUploadReverse._SimpleResponse":
"""使用标准库 urllib 兜底上传,绕过 curl_cffi 异常。"""
body = json.dumps(payload).encode("utf-8")
opener = None
if proxy_url:
opener = urllib.request.build_opener(
urllib.request.ProxyHandler({"http": proxy_url, "https": proxy_url})
)
req = urllib.request.Request(url=url, data=body, headers=headers, method="POST")
def _do_post():
def _read_http_error(err: urllib.error.HTTPError):
status = int(getattr(err, "code", 500) or 500)
hdrs = {}
try:
hdrs = {
str(k).lower(): str(v)
for k, v in dict((err.headers or {}).items()).items()
}
except Exception:
hdrs = {}
try:
body = err.read().decode("utf-8", errors="replace")
except Exception:
body = ""
return status, hdrs, body
if opener is not None:
try:
with opener.open(req, timeout=timeout) as resp:
status = int(getattr(resp, "status", 200) or 200)
raw_headers = {
str(k).lower(): str(v)
for k, v in dict(resp.headers.items()).items()
}
text = resp.read().decode("utf-8", errors="replace")
return status, raw_headers, text
except urllib.error.HTTPError as e:
return _read_http_error(e)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
status = int(getattr(resp, "status", 200) or 200)
raw_headers = {
str(k).lower(): str(v) for k, v in dict(resp.headers.items()).items()
}
text = resp.read().decode("utf-8", errors="replace")
return status, raw_headers, text
except urllib.error.HTTPError as e:
return _read_http_error(e)
status, raw_headers, text = await asyncio.to_thread(_do_post)
return AssetsUploadReverse._SimpleResponse(
status_code=status,
headers=raw_headers,
text=text,
)
@staticmethod
async def request(session: AsyncSession, token: str, fileName: str, fileMimeType: str, content: str) -> Any:
"""Upload asset to Grok.
Args:
session: AsyncSession, the session to use for the request.
token: str, the SSO token.
fileName: str, the name of the file.
fileMimeType: str, the MIME type of the file.
content: str, the content of the file.
Returns:
Any: The response from the request.
"""
try:
# Get proxies
base_proxy = get_config("proxy.base_proxy_url")
assert_proxy = get_config("proxy.asset_proxy_url")
if assert_proxy:
proxies = {"http": assert_proxy, "https": assert_proxy}
proxy_url = assert_proxy
else:
proxies = {"http": base_proxy, "https": base_proxy}
proxy_url = base_proxy
# Build headers
headers = build_headers(
cookie_token=token,
content_type="application/json",
origin="https://grok.com",
referer="https://grok.com/",
)
# Build payload
payload = {
"fileName": fileName,
"fileMimeType": fileMimeType,
"content": content,
}
logger.info(
"AssetsUpload request prepared: "
f"fileName={fileName}, fileMimeType={fileMimeType}, content_len={len(content or '')}"
)
# Curl Config
timeout = get_config("asset.upload_timeout")
browser = get_config("proxy.browser")
async def _do_request():
try:
try:
response = await session.post(
UPLOAD_API,
headers=headers,
json=payload,
proxies=proxies,
timeout=timeout,
impersonate=browser,
)
except Exception as first_err:
logger.warning(
"AssetsUploadReverse primary request failed, fallback direct: "
f"error={first_err}"
)
try:
response = await session.post(
UPLOAD_API,
headers=headers,
json=payload,
timeout=timeout,
)
except Exception as second_err:
logger.warning(
"AssetsUploadReverse direct curl request failed, "
f"fallback urllib: error={second_err}"
)
response = await AssetsUploadReverse._urllib_post(
url=UPLOAD_API,
headers=headers,
payload=payload,
timeout=timeout,
proxy_url=proxy_url,
)
if response.status_code != 200:
body_preview = ""
try:
body_preview = (response.text or "").strip().replace("\n", " ")
except Exception:
body_preview = ""
if len(body_preview) > 300:
body_preview = f"{body_preview[:300]}...(len={len(body_preview)})"
logger.error(
"AssetsUploadReverse: Upload failed, "
f"status={response.status_code}, body={body_preview or '-'}",
extra={"error_type": "UpstreamException"},
)
raise UpstreamException(
message=f"AssetsUploadReverse: Upload failed, {response.status_code}",
details={"status": response.status_code, "body": body_preview},
)
return response
except UpstreamException:
raise
except Exception as inner_err:
err_msg = str(inner_err)
# 这类异常在实际环境里多为瞬时网络/上游抖动,按可重试处理。
if "curl: (35)" in err_msg or '"code"' in err_msg:
logger.warning(
"AssetsUpload transient exception, mark as retryable: "
f"{err_msg}"
)
last_fallback_error: UpstreamException | None = None
# 部分 '"code"' 异常不会走到上面的降级分支,这里再强制兜底一次。
try:
response = await session.post(
UPLOAD_API,
headers=headers,
json=payload,
timeout=timeout,
)
if response.status_code == 200:
logger.info(
"AssetsUpload recovered by forced direct fallback after transient error"
)
return response
body_preview = ""
try:
body_preview = (response.text or "").strip().replace("\n", " ")
except Exception:
body_preview = ""
if len(body_preview) > 300:
body_preview = f"{body_preview[:300]}...(len={len(body_preview)})"
raise UpstreamException(
message=f"AssetsUpload forced direct failed: {response.status_code}",
details={"status": response.status_code, "body": body_preview},
)
except Exception as forced_direct_err:
if isinstance(forced_direct_err, UpstreamException):
last_fallback_error = forced_direct_err
logger.warning(
"AssetsUpload forced direct fallback failed, "
f"error={forced_direct_err}"
)
try:
response = await AssetsUploadReverse._urllib_post(
url=UPLOAD_API,
headers=headers,
payload=payload,
timeout=timeout,
proxy_url=proxy_url,
)
if response.status_code == 200:
logger.info(
"AssetsUpload recovered by forced urllib fallback after transient error"
)
return response
body_preview = (response.text or "").strip().replace("\n", " ")
if len(body_preview) > 300:
body_preview = f"{body_preview[:300]}...(len={len(body_preview)})"
raise UpstreamException(
message=f"AssetsUpload forced urllib failed: {response.status_code}",
details={"status": response.status_code, "body": body_preview},
)
except Exception as forced_urllib_err:
if isinstance(forced_urllib_err, UpstreamException):
last_fallback_error = forced_urllib_err
logger.warning(
"AssetsUpload forced urllib fallback failed, "
f"error={forced_urllib_err}"
)
if last_fallback_error is not None:
raise last_fallback_error
raise UpstreamException(
message=f"AssetsUpload transient failure: {err_msg}",
details={"status": 403, "error": err_msg},
)
raise
return await retry_on_status(_do_request)
except Exception as e:
# Handle upstream exception
if isinstance(e, UpstreamException):
status = None
if e.details and "status" in e.details:
status = e.details["status"]
else:
status = getattr(e, "status_code", None)
if status == 401:
try:
await TokenService.record_fail(token, status, "assets_upload_auth_failed")
except Exception:
pass
raise
# Handle other non-upstream exceptions
logger.error(
f"AssetsUploadReverse: Upload failed, {str(e)}",
extra={"error_type": type(e).__name__},
)
raise UpstreamException(
message=f"AssetsUploadReverse: Upload failed, {str(e)}",
details={"status": 502, "error": str(e)},
)
__all__ = ["AssetsUploadReverse"]