tripocli v2.0.0:Tripo AI 3D 生成与加工 CLI(面向 Agent 与开发者,纯 Python 标准库、零第三方依赖)

覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、
纹理、PBR、智能 UV、部件补全、格式导出与下载。

代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
This commit is contained in:
Mrpzx committed 2026-10-13 19:22:54 +08:00
commit 977f97995a
60 files changed
+5622

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
"""Mint the short-lived JWT that the API needs, from a session cookie.
The web session cookie (``ory_kratos_session``, ~30 days) authenticates
``GET /v2/studio/studio/whoami?tokenizeAs=default_jwt`` over plain HTTP and
returns a ``tokenized`` JWT valid for ~15 minutes. Verified live: a cookie alone
is enough — no browser, no device id.
The endpoint occasionally answers a transient ``401`` under rapid repeated calls,
so minting retries a few times with a short backoff before giving up.
"""
import base64
import json
import time
import urllib.error
import urllib.request
from .errors import TripoAPIError
ORIGIN = "https://studio.tripo3d.ai"
COOKIE_NAME = "ory_kratos_session"
WHOAMI = "/v2/studio/studio/whoami?tokenizeAs=default_jwt"
RETRY_ATTEMPTS = 3
RETRY_BACKOFF = 1.5
def _request(api_base, cookie, device_id=None, timeout=30):
headers = {
"Accept": "application/json",
"Origin": ORIGIN,
"Referer": ORIGIN + "/",
"User-Agent": "tripocli/2.0.0",
"Cookie": cookie,
}
if device_id:
headers["x-tripo-device-id"] = device_id
request = urllib.request.Request(f"{api_base}{WHOAMI}", headers=headers, method="GET")
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", "replace")
raise TripoAPIError(
f"会话换取 JWT 失败 (HTTP {error.code}): {body[:200]}", code=error.code
) from None
except urllib.error.URLError as error:
raise TripoAPIError(f"网络错误: {error.reason}") from None
def decode_jwt_payload(token):
"""Return the JWT payload, or ``{}`` when it cannot be decoded."""
try:
parts = (token or "").split(".")
if len(parts) >= 2:
payload = parts[1] + "=" * (-len(parts[1]) % 4)
return json.loads(base64.urlsafe_b64decode(payload).decode("utf-8"))
except Exception:
pass
return {}
def normalize_cookie(cookie):
"""Accept either a bare cookie value or a full ``name=value; name2=value2`` string."""
cookie = (cookie or "").strip().strip('"').strip("'")
if not cookie:
raise ValueError("cookie 为空")
if "=" not in cookie:
cookie = f"{COOKIE_NAME}={cookie}"
return cookie
def fetch_identity(api_base, cookie, device_id=None):
"""Read ``(user_id, email)`` for a session cookie.
Note ``/studio/whoami`` authenticates with the *cookie*, not the Bearer JWT
(a JWT-only call answers 401), so identity lookup goes through the same
cookie path as minting.
"""
cookie = normalize_cookie(cookie)
data = _request(api_base, cookie, device_id)
traits = (data.get("identity") or {}).get("traits") or {}
user_id = data.get("id") or decode_jwt_payload(data.get("tokenized") or "").get("sub")
return user_id, traits.get("email")
def mint_token(api_base, cookie, device_id=None):
"""Exchange a session cookie for ``(token, session_expires_at)``."""
cookie = normalize_cookie(cookie)
last_error = None
for attempt in range(1, RETRY_ATTEMPTS + 1):
try:
data = _request(api_base, cookie, device_id)
except TripoAPIError as error:
last_error = error
# 401 here is usually a transient throttle; 5xx/network also worth retrying.
if error.code in (401, 403, 429, 500, 502, 503, 504, None) and attempt < RETRY_ATTEMPTS:
time.sleep(RETRY_BACKOFF * attempt)
continue
raise
token = data.get("tokenized")
if not token:
raise TripoAPIError("会话有效但未返回 tokenized,cookie 可能已失效", raw=data)
return token, data.get("expires_at")
raise last_error