覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、 纹理、PBR、智能 UV、部件补全、格式导出与下载。 代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
133 lines
4.1 KiB
Python
133 lines
4.1 KiB
Python
"""Multi-account credential store.
|
|
|
|
A user hands the CLI a session cookie once (paste it, or pass ``TRIPO_COOKIE``);
|
|
from then on the CLI mints short-lived JWTs on demand over plain HTTP — no
|
|
browser, no repeated login.
|
|
|
|
~/.tripo/accounts.json
|
|
{
|
|
"active": "main",
|
|
"accounts": {
|
|
"main": {"cookie": "...", "token": "...", "session_expires_at": "...",
|
|
"user_id": "...", "email": "...", "added_at": 1791800000}
|
|
}
|
|
}
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
|
|
DEFAULT_ACCOUNTS_PATH = Path.home() / ".tripo" / "accounts.json"
|
|
|
|
|
|
class Accounts:
|
|
def __init__(self, path=None):
|
|
self.path = Path(path) if path else DEFAULT_ACCOUNTS_PATH
|
|
self.data = {"active": None, "accounts": {}}
|
|
self.load()
|
|
|
|
# --- persistence ---
|
|
|
|
def load(self):
|
|
if self.path.exists():
|
|
try:
|
|
loaded = json.loads(self.path.read_text(encoding="utf-8"))
|
|
self.data = {
|
|
"active": loaded.get("active"),
|
|
"accounts": loaded.get("accounts", {}),
|
|
}
|
|
except Exception:
|
|
self.data = {"active": None, "accounts": {}}
|
|
|
|
def save(self):
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self.path.write_text(json.dumps(self.data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
try:
|
|
self.path.chmod(0o600) # holds a session cookie: keep it private
|
|
except OSError:
|
|
pass
|
|
|
|
# --- queries ---
|
|
|
|
def names(self):
|
|
return sorted(self.data["accounts"])
|
|
|
|
def rotation(self, start=None):
|
|
"""Account names starting at ``start`` (default: active), wrapping around.
|
|
|
|
Used by the optional auto-fallback so a command can try the next account
|
|
when the current one is out of credits.
|
|
"""
|
|
names = self.names()
|
|
if not names:
|
|
return []
|
|
start = start or self.data.get("active")
|
|
if start not in names:
|
|
return names
|
|
index = names.index(start)
|
|
return names[index:] + names[:index]
|
|
|
|
def exists(self, name):
|
|
return name in self.data["accounts"]
|
|
|
|
def active_name(self):
|
|
return self.data.get("active")
|
|
|
|
def get(self, name=None):
|
|
"""Return ``(name, record)`` for ``name`` or the active account."""
|
|
name = name or self.data.get("active")
|
|
if not name or name not in self.data["accounts"]:
|
|
return None, None
|
|
return name, self.data["accounts"][name]
|
|
|
|
# --- mutations ---
|
|
|
|
def add(self, name, cookie, token=None, session_expires_at=None, user_id=None, email=None):
|
|
record = self.data["accounts"].get(name, {})
|
|
record.update({
|
|
"cookie": cookie,
|
|
"token": token,
|
|
"session_expires_at": session_expires_at,
|
|
"user_id": user_id,
|
|
"email": email,
|
|
"added_at": record.get("added_at") or int(time.time()),
|
|
"updated_at": int(time.time()),
|
|
})
|
|
self.data["accounts"][name] = record
|
|
if not self.data.get("active"):
|
|
self.data["active"] = name
|
|
self.save()
|
|
return name
|
|
|
|
def use(self, name):
|
|
if name not in self.data["accounts"]:
|
|
raise KeyError(name)
|
|
self.data["active"] = name
|
|
self.save()
|
|
|
|
def remove(self, name):
|
|
if name not in self.data["accounts"]:
|
|
raise KeyError(name)
|
|
del self.data["accounts"][name]
|
|
if self.data.get("active") == name:
|
|
self.data["active"] = self.names()[0] if self.data["accounts"] else None
|
|
self.save()
|
|
|
|
def update_token(self, name, token, session_expires_at=None):
|
|
record = self.data["accounts"].get(name)
|
|
if record is None:
|
|
return
|
|
record["token"] = token
|
|
record["updated_at"] = int(time.time())
|
|
if session_expires_at:
|
|
record["session_expires_at"] = session_expires_at
|
|
self.save()
|
|
|
|
def update_cookie(self, name, cookie):
|
|
record = self.data["accounts"].get(name)
|
|
if record is None:
|
|
return
|
|
record["cookie"] = cookie
|
|
self.save()
|