"""账号管理 — add / list / use / remove / show. 给别人用时不需要浏览器:把浏览器的 cookie 粘贴进来一次,CLI 之后自己续期 JWT。 tripocli account add --name work --cookie "ory_kratos_session=...; csrf_token_...=..." tripocli account add # 交互式粘贴 tripocli account list tripocli account use work tripocli account remove work """ import getpass import sys import time from .. import auth from ..accounts import Accounts from ..config import Config COOKIE_HINT = ( "请粘贴 cookie(整条 Cookie 字符串,或仅 ory_kratos_session 的值),回车结束:\n" " 提示:浏览器 DevTools → Network → 任意 api.tripo3d.ai 请求 → Request Headers → Cookie 整条复制" ) def _fmt(epoch): if not epoch: return None if isinstance(epoch, (int, float)): return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(epoch)) return str(epoch) def _mask(value, keep=10): if not value: return None return f"{value[:keep]}...{value[-6:]}" if len(value) > keep + 8 else "***" def _read_cookie(args): if args.cookie_file: with open(args.cookie_file, "r", encoding="utf-8") as handle: return handle.read().strip() if args.cookie == "-": return sys.stdin.read().strip() if args.cookie: return args.cookie.strip() # Interactive paste; hidden input so the cookie does not land in scrollback. print(COOKIE_HINT) try: return getpass.getpass("cookie: ").strip() except (EOFError, KeyboardInterrupt): raise SystemExit("已取消") def _identity(api_base, cookie, device_id): """Best-effort identity lookup so accounts are recognisable in `account list`.""" try: return auth.fetch_identity(api_base, cookie, device_id) except Exception: return None, None def _add(ctx): args = ctx.args cookie = auth.normalize_cookie(_read_cookie(args)) # Validate before storing: a bad cookie must fail here, not on the next command. token, session_expires = auth.mint_token(ctx.config.api_base, cookie, ctx.config.device_id) user_id, email = _identity(ctx.config.api_base, cookie, ctx.config.device_id) name = args.name or email or user_id or "default" ctx.accounts.add(name, cookie, token=token, session_expires_at=session_expires, user_id=user_id, email=email) payload = { "added": name, "active": ctx.accounts.active_name(), "email": email, "user_id": user_id, "session_expires_at": _fmt(session_expires), } ctx.out.data( payload, human=lambda: ( ctx.out.line(f"✅ 账号已保存: {name}"), ctx.out.line(f"身份 : {email or user_id or '未知'}"), ctx.out.line(f"会话有效期: {_fmt(session_expires) or '未知'}"), ctx.out.line("之后无需浏览器,CLI 会自动用 cookie 续期 JWT"), ), ) return payload def _list(ctx): accounts = ctx.accounts active = accounts.active_name() items = [] for name in accounts.names(): record = accounts.data["accounts"][name] items.append({ "name": name, "active": name == active, "email": record.get("email"), "user_id": record.get("user_id"), "session_expires_at": _fmt(record.get("session_expires_at")), "cookie": _mask(record.get("cookie")), }) def human(): if not items: ctx.out.line("暂无账号。用 `tripocli account add` 粘贴 cookie 添加。") return for item in items: mark = "*" if item["active"] else " " ctx.out.line(f"{mark} {item['name']:<20} {item['email'] or item['user_id'] or '-'}" f" (会话至 {item['session_expires_at'] or '未知'})") ctx.out.data({"active": active, "accounts": items}, human=human) return items def _use(ctx): name = ctx.args.name try: ctx.accounts.use(name) except KeyError: raise ValueError(f"账号不存在: {name}(可用 `tripocli account list` 查看)") ctx.out.data({"active": name}, human=lambda: ctx.out.line(f"✅ 当前账号已切换为 {name}")) return {"active": name} def _remove(ctx): name = ctx.args.name try: ctx.accounts.remove(name) except KeyError: raise ValueError(f"账号不存在: {name}") payload = {"removed": name, "active": ctx.accounts.active_name()} ctx.out.data(payload, human=lambda: ctx.out.line(f"✅ 已删除账号 {name}")) return payload def _show(ctx): name = ctx.args.name resolved, record = ctx.accounts.get(name) if not record: raise ValueError(f"账号不存在: {name or '(无当前账号)'}") info = { "name": resolved, "active": resolved == ctx.accounts.active_name(), "email": record.get("email"), "user_id": record.get("user_id"), "cookie": _mask(record.get("cookie")), "session_expires_at": _fmt(record.get("session_expires_at")), "added_at": _fmt(record.get("added_at")), } ctx.out.data(info, human=lambda: ( ctx.out.line(f"账号 : {resolved}{' (当前)' if info['active'] else ''}"), ctx.out.line(f"身份 : {record.get('email') or record.get('user_id') or '未知'}"), ctx.out.line(f"Cookie : {info['cookie']}"), ctx.out.line(f"会话有效期: {info['session_expires_at'] or '未知'}"), )) return info def register(subparsers): parser = subparsers.add_parser("account", help="账号管理(粘贴 cookie,多账号切换)") sub = parser.add_subparsers(dest="account_action", required=True) add = sub.add_parser("add", help="添加账号(粘贴 cookie,无需浏览器)") add.add_argument("--name", help="账号名(默认取邮箱/用户 id)") add.add_argument("--cookie", help="cookie 值,或整条 Cookie 串;传 - 从 stdin 读取") add.add_argument("--cookie-file", help="从文件读取 cookie") add.set_defaults(func=_add) sub.add_parser("list", help="列出所有账号").set_defaults(func=_list) use = sub.add_parser("use", help="切换当前账号") use.add_argument("name") use.set_defaults(func=_use) remove = sub.add_parser("remove", help="删除账号") remove.add_argument("name") remove.set_defaults(func=_remove) show = sub.add_parser("show", help="查看账号详情") show.add_argument("name", nargs="?") show.set_defaults(func=_show)