覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、 纹理、PBR、智能 UV、部件补全、格式导出与下载。 代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
169 lines
6.9 KiB
Python
169 lines
6.9 KiB
Python
"""argparse wiring and dispatch.
|
||
|
||
``tripocli`` is a thin shell: this module builds the command tree from the
|
||
feature modules, resolves credentials (re-minting the JWT from the account's
|
||
session cookie when needed) and hands control to the selected feature.
|
||
"""
|
||
|
||
import argparse
|
||
import os
|
||
import sys
|
||
|
||
from . import __version__
|
||
from .accounts import Accounts
|
||
from .client import TripoClient
|
||
from .config import Config
|
||
from .errors import TripoAPIError, TripoTimeout, is_insufficient_credits
|
||
from .features import MODULES
|
||
from .output import Out
|
||
|
||
EPILOG = """\
|
||
常用示例:
|
||
tripocli account add --cookie "<粘贴浏览器 cookie>" # 一次性添加账号,之后免浏览器
|
||
tripocli account list # 多账号查看/切换
|
||
tripocli serve --open # 打开网页版账号管理(看积分/有效期)
|
||
tripocli update # 改完源码后更新全局 CLI
|
||
tripocli balance # 查看积分
|
||
tripocli text -p "a wooden barrel" --wait -o a.glb # 文生 3D
|
||
tripocli image -i ./photo.png --wait -o b.glb # 单图生 3D
|
||
tripocli multiview --front f.png --left l.png --right r.png --back b.png --wait -o c.glb
|
||
tripocli batch -i a.png,b.png --wait -o ./out # 批量图生 3D
|
||
tripocli remesh -p <project_id> --face-limit 8000 --wait -o low.glb
|
||
tripocli rig -p <project_id> --type biped --wait # 绑定骨骼
|
||
tripocli animate -p <project_id> --preset idle --wait -o anim.glb
|
||
tripocli segment -p <project_id> --wait # 智能分割
|
||
tripocli texture -p <project_id> --resolution 2K --wait
|
||
tripocli pbr -p <project_id> --wait # PBR 材质
|
||
tripocli upscale -p <project_id> --wait # 纹理放大
|
||
tripocli export -p <project_id> -f fbx --wait -o m.fbx
|
||
|
||
凭据:只需提供一次浏览器 cookie,CLI 会自行续期 JWT(cookie 约 30 天有效)。
|
||
也可用环境变量 TRIPO_COOKIE 提供;多账号用 --account <名字> 指定。
|
||
加 --json 可输出机器可读结果(Agent 调用推荐)。
|
||
"""
|
||
|
||
# Commands that must run without any credentials.
|
||
NO_CREDENTIAL_COMMANDS = {"account", "config", "serve", "update", "inspect", "optimize"}
|
||
|
||
|
||
class Context:
|
||
"""Everything a feature needs: client, config, accounts, output sink and args."""
|
||
|
||
def __init__(self, client, config, accounts, out, args):
|
||
self.client = client
|
||
self.config = config
|
||
self.accounts = accounts
|
||
self.out = out
|
||
self.args = args
|
||
|
||
|
||
def _add_json_flag_everywhere(parser):
|
||
"""Allow ``--json`` after the subcommand as well as before it.
|
||
|
||
``argparse`` only accepts a parent parser's options before the subcommand, but
|
||
agents habitually append ``--json`` at the end; SUPPRESS keeps the subcommand
|
||
copy from overwriting a value given on the main parser.
|
||
"""
|
||
for action in parser._actions:
|
||
if isinstance(action, argparse._SubParsersAction):
|
||
for subparser in action.choices.values():
|
||
if "--json" not in subparser._option_string_actions:
|
||
subparser.add_argument(
|
||
"--json", action="store_true", default=argparse.SUPPRESS,
|
||
help="输出结构化 JSON(Agent 调用推荐)",
|
||
)
|
||
_add_json_flag_everywhere(subparser)
|
||
|
||
|
||
def build_parser():
|
||
parser = argparse.ArgumentParser(
|
||
prog="tripocli",
|
||
description="Tripo AI 3D 生成 CLI(面向 Agent 与开发者)",
|
||
epilog=EPILOG,
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
)
|
||
parser.add_argument("--json", action="store_true", help="输出结构化 JSON(Agent 调用推荐)")
|
||
parser.add_argument("--account", help="使用指定账号(默认用当前账号)")
|
||
parser.add_argument(
|
||
"--no-fallback", action="store_true",
|
||
help="关闭「额度不足自动切换下一个账号」(默认开启;设 TRIPO_AUTO_FALLBACK=0 也可关闭)",
|
||
)
|
||
parser.add_argument("--token", help="临时覆盖 Bearer JWT")
|
||
parser.add_argument("--device-id", help="临时覆盖 x-tripo-device-id")
|
||
parser.add_argument("--api-base", help="临时覆盖 API 根地址")
|
||
parser.add_argument("-V", "--version", action="version", version=f"tripocli {__version__}")
|
||
|
||
subparsers = parser.add_subparsers(dest="command", metavar="<命令>")
|
||
for module in MODULES:
|
||
if hasattr(module, "register"):
|
||
module.register(subparsers)
|
||
else:
|
||
command = subparsers.add_parser(module.NAME, help=module.HELP)
|
||
module.add_arguments(command)
|
||
command.set_defaults(func=module.run)
|
||
_add_json_flag_everywhere(parser)
|
||
return parser
|
||
|
||
|
||
def main(argv=None):
|
||
parser = build_parser()
|
||
args = parser.parse_args(argv)
|
||
|
||
handler = getattr(args, "func", None)
|
||
if handler is None:
|
||
parser.print_help()
|
||
return 0
|
||
|
||
out = Out(as_json=args.json)
|
||
config = Config()
|
||
accounts = Accounts()
|
||
|
||
needs_credentials = args.command not in NO_CREDENTIAL_COMMANDS
|
||
# 默认开启账号回退;显式 --no-fallback 或 TRIPO_AUTO_FALLBACK=0 可关闭。
|
||
env_fallback = os.environ.get("TRIPO_AUTO_FALLBACK", "").strip().lower()
|
||
fallback = not args.no_fallback and env_fallback not in ("0", "false", "no")
|
||
if needs_credentials and fallback and len(accounts.names()) > 1:
|
||
order = accounts.rotation(args.account)
|
||
else:
|
||
order = [args.account]
|
||
extra_codes = config.data.get("insufficient_credit_codes") or []
|
||
|
||
last_error = None
|
||
for index, account in enumerate(order):
|
||
client = TripoClient(
|
||
token=args.token, device_id=args.device_id, api_base=args.api_base,
|
||
account=account, accounts=accounts, config=config,
|
||
)
|
||
ctx = Context(client=client, config=config, accounts=accounts, out=out, args=args)
|
||
try:
|
||
if needs_credentials:
|
||
client.ensure_credentials()
|
||
handler(ctx)
|
||
return 0
|
||
except KeyboardInterrupt:
|
||
out.error("已中断")
|
||
return 130
|
||
except (TripoAPIError, TripoTimeout, ValueError, FileNotFoundError, RuntimeError) as error:
|
||
can_retry = (
|
||
needs_credentials
|
||
and fallback
|
||
and index + 1 < len(order)
|
||
and is_insufficient_credits(error, extra_codes)
|
||
)
|
||
if can_retry:
|
||
out.notice(f"⚠️ 账号 {account or accounts.active_name()} 额度不足,切换到下一个账号重试…")
|
||
last_error = error
|
||
continue
|
||
out.error(error)
|
||
return 1
|
||
except Exception as error: # unexpected: keep stdout parseable for agents
|
||
out.error(f"运行错误: {error}")
|
||
return 1
|
||
|
||
out.error(last_error or "没有可用账号")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|