304 lines
13 KiB
Python
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"]
|