"""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 --face-limit 8000 --wait -o low.glb tripocli rig -p --type biped --wait # 绑定骨骼 tripocli animate -p --preset idle --wait -o anim.glb tripocli segment -p --wait # 智能分割 tripocli texture -p --resolution 2K --wait tripocli pbr -p --wait # PBR 材质 tripocli upscale -p --wait # 纹理放大 tripocli export -p -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())