Spaces:
Sleeping
Sleeping
| """CLI entry point for chatgpt-register-sub2api. | |
| Subcommands: | |
| init Write default config.yaml | |
| register Register N ChatGPT accounts | |
| join-workspace Join registered accounts to K12 workspace | |
| login-team Re-login with Team space selection | |
| export Export to sub2api JSON | |
| run Full pipeline (register → join → login → export) | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import logging | |
| import sys | |
| from pathlib import Path | |
| from chatgpt_register_sub2api import __version__ | |
| from chatgpt_register_sub2api.config import ( | |
| DEFAULT_CONFIG_FILE, | |
| generate_default_config, | |
| load_config, | |
| ) | |
| from chatgpt_register_sub2api.pipeline import ( | |
| load_accounts, | |
| run_export, | |
| run_full_pipeline, | |
| run_join_workspace, | |
| run_re_login, | |
| run_register, | |
| save_accounts, | |
| ) | |
| from chatgpt_register_sub2api.export.checker import check_sub2api_file | |
| from chatgpt_register_sub2api.export.cpa import export_cpa_json_files, export_cpa_zip | |
| from chatgpt_register_sub2api.pool import clean_pool | |
| from chatgpt_register_sub2api.cdk import generate_cdk | |
| def setup_logging(config: dict, verbose: bool = False) -> None: | |
| """Configure Python logging based on config + CLI verbosity.""" | |
| log_cfg = config.get("logging", {}) | |
| level_name = "DEBUG" if verbose else str(log_cfg.get("level", "INFO")).upper() | |
| level = getattr(logging, level_name, logging.INFO) | |
| fmt = "%(asctime)s [%(levelname)-5s] %(message)s" | |
| datefmt = "%H:%M:%S" | |
| log_file = str(log_cfg.get("file", "")).strip() | |
| if log_file: | |
| logging.basicConfig( | |
| level=level, | |
| format=fmt, | |
| datefmt=datefmt, | |
| handlers=[ | |
| logging.FileHandler(log_file, encoding="utf-8"), | |
| logging.StreamHandler(sys.stderr), | |
| ], | |
| ) | |
| else: | |
| logging.basicConfig( | |
| level=level, | |
| format=fmt, | |
| datefmt=datefmt, | |
| stream=sys.stderr, | |
| ) | |
| def cmd_init(args) -> int: | |
| """Write default config.yaml.""" | |
| path = Path(args.config) if args.config else DEFAULT_CONFIG_FILE | |
| try: | |
| output = generate_default_config(path) | |
| print(f"Config written to {output}") | |
| return 0 | |
| except FileExistsError as e: | |
| print(f"Error: {e}", file=sys.stderr) | |
| return 1 | |
| def cmd_cdk(args) -> int: | |
| import datetime as dt | |
| expires = args.expires | |
| if not expires: | |
| expires = ( | |
| dt.datetime.now(dt.timezone(dt.timedelta(hours=8))) | |
| + dt.timedelta(days=max(0, int(args.days))) | |
| ).strftime("%Y%m%d") | |
| print(generate_cdk(args.secret, expires=expires, max_count=args.max_count, nonce=args.nonce)) | |
| return 0 | |
| def cmd_register(args) -> int: | |
| """Register N ChatGPT accounts.""" | |
| config = load_config(args.config) | |
| setup_logging(config, args.verbose) | |
| logger = logging.getLogger(__name__) | |
| config_dir = Path(config.get("_config_dir", ".")) | |
| accounts_file = config_dir / "registered_accounts.json" | |
| results = run_register( | |
| config=config, | |
| accounts_file=accounts_file, | |
| count=args.count, | |
| ) | |
| print(f"\nRegistered: {len(results)} accounts") | |
| for acc in results: | |
| print(f" {acc['email']}") | |
| return 0 | |
| def cmd_join_workspace(args) -> int: | |
| """Join registered accounts to K12 workspace.""" | |
| config = load_config(args.config) | |
| setup_logging(config, args.verbose) | |
| logger = logging.getLogger(__name__) | |
| # Override workspace IDs from CLI | |
| if args.workspace_id: | |
| config.setdefault("workspace", {})["ids"] = args.workspace_id | |
| config_dir = Path(config.get("_config_dir", ".")) | |
| input_file = Path(args.input) if args.input else config_dir / "registered_accounts.json" | |
| accounts = load_accounts(input_file) | |
| if not accounts: | |
| print(f"No accounts found in {input_file}", file=sys.stderr) | |
| return 1 | |
| accounts = run_join_workspace(config, accounts) | |
| save_accounts(input_file, accounts) | |
| joined = sum(1 for a in accounts if a.get("join_status") == "ok") | |
| print(f"\nJoined: {joined}/{len(accounts)} accounts") | |
| return 0 | |
| def cmd_login_team(args) -> int: | |
| """Re-login with Team space selection.""" | |
| config = load_config(args.config) | |
| setup_logging(config, args.verbose) | |
| logger = logging.getLogger(__name__) | |
| config_dir = Path(config.get("_config_dir", ".")) | |
| input_file = Path(args.input) if args.input else config_dir / "registered_accounts.json" | |
| accounts = load_accounts(input_file) | |
| if not accounts: | |
| print(f"No accounts found in {input_file}", file=sys.stderr) | |
| return 1 | |
| accounts = run_re_login(config, accounts) | |
| save_accounts(input_file, accounts) | |
| team_logged = sum(1 for a in accounts if a.get("team_login_status") == "ok") | |
| print(f"\nTeam logged: {team_logged}/{len(accounts)} accounts") | |
| return 0 | |
| def cmd_export(args) -> int: | |
| """Export to sub2api JSON.""" | |
| config = load_config(args.config) | |
| setup_logging(config, args.verbose) | |
| logger = logging.getLogger(__name__) | |
| config_dir = Path(config.get("_config_dir", ".")) | |
| input_file = Path(args.input) if args.input else config_dir / "registered_accounts.json" | |
| accounts = load_accounts(input_file) | |
| if not accounts: | |
| print(f"No accounts found in {input_file}", file=sys.stderr) | |
| return 1 | |
| output_file = Path(args.output) if args.output else None | |
| json_str = run_export(config, accounts, output_file) | |
| actual_path = str(config.get("_last_export_file") or output_file or "") | |
| if args.stdout: | |
| print(json_str) | |
| else: | |
| print(f"Exported to {actual_path}") | |
| if args.check: | |
| report = _check_exported_file(config, actual_path, args) | |
| _print_check_report(report) | |
| return 0 if report["summary"]["failed"] == 0 else 2 | |
| return 0 | |
| def _resolve_check_input(config: dict, input_path: str | None = None) -> Path: | |
| """Resolve a sub2api JSON file for live-check.""" | |
| config_dir = Path(config.get("_config_dir", ".")) | |
| if input_path: | |
| return Path(input_path) | |
| configured = str(config.get("sub2api", {}).get("output_file") or "").strip() | |
| if configured: | |
| path = Path(configured) | |
| if not path.is_absolute(): | |
| path = config_dir / path | |
| if path.exists(): | |
| return path | |
| candidates = sorted( | |
| config_dir.glob("sub2api*.json"), | |
| key=lambda item: item.stat().st_mtime, | |
| reverse=True, | |
| ) | |
| if candidates: | |
| return candidates[0] | |
| return (config_dir / configured) if configured else config_dir / "sub2api_bundle.json" | |
| def _check_exported_file(config: dict, path: str | Path, args) -> dict: | |
| """Run direct /v1/me checks for an exported sub2api JSON file.""" | |
| check_path = Path(path) | |
| if not check_path.exists(): | |
| raise FileNotFoundError(f"Sub2API JSON not found: {check_path}") | |
| proxy = str(config.get("proxy", {}).get("url") or "").strip() | |
| return check_sub2api_file( | |
| check_path, | |
| timeout=float(getattr(args, "timeout", 15)), | |
| retries=int(getattr(args, "retries", 2)), | |
| proxy=proxy, | |
| limit=getattr(args, "limit", None), | |
| ) | |
| def _print_check_report(report: dict) -> None: | |
| summary = report.get("summary", {}) | |
| print(f"\nSub2API Live Check: {report.get('path', '')}") | |
| print( | |
| " Total: {total} OK: {ok} Failed: {failed} " | |
| "401: {unauthorized_401} Auth404: {authenticated_404} " | |
| "Expired(local): {expired_local} " | |
| "Temporary: {temporary_error}".format(**summary) | |
| ) | |
| for item in report.get("results", []): | |
| http_status = item.get("http_status") | |
| http_text = f"HTTP {http_status}" if http_status is not None else "local" | |
| exp = item.get("exp") or "-" | |
| identity = item.get("email") or item.get("name") or f"#{item.get('index')}" | |
| print( | |
| f" [{item.get('index')}] {item.get('status')} " | |
| f"{http_text}, attempts={item.get('attempts')}, exp={exp}, {identity}" | |
| ) | |
| if not item.get("ok") and item.get("error"): | |
| print(f" {str(item.get('error'))[:200]}") | |
| def cmd_check(args) -> int: | |
| """Live-check an exported sub2api JSON file.""" | |
| config = load_config(args.config) | |
| setup_logging(config, args.verbose) | |
| input_file = _resolve_check_input(config, args.input) | |
| try: | |
| report = _check_exported_file(config, input_file, args) | |
| except FileNotFoundError as error: | |
| print(str(error), file=sys.stderr) | |
| return 1 | |
| _print_check_report(report) | |
| return 0 if report["summary"]["failed"] == 0 else 2 | |
| def cmd_export_cpa(args) -> int: | |
| """Export one CPA/CLIProxyAPI Codex JSON per account and optional ZIP.""" | |
| config = load_config(args.config) | |
| setup_logging(config, args.verbose) | |
| config_dir = Path(config.get("_config_dir", ".")) | |
| input_file = Path(args.input) if args.input else config_dir / "registered_accounts.json" | |
| accounts = load_accounts(input_file) | |
| if not accounts: | |
| print(f"No accounts found in {input_file}", file=sys.stderr) | |
| return 1 | |
| output_dir = Path(args.output_dir) if args.output_dir else config_dir / "cpa_auths" | |
| written = export_cpa_json_files(accounts, output_dir) | |
| print(f"CPA JSON exported: {len(written)} files -> {output_dir.resolve()}") | |
| zip_path = None | |
| if args.zip: | |
| zip_path = export_cpa_zip(accounts, args.zip) | |
| print(f"CPA ZIP exported: {zip_path}") | |
| if args.prune_config: | |
| state_file = config_dir / "data" / "outlook_token_state.json" | |
| result = clean_pool( | |
| config_file=args.config or DEFAULT_CONFIG_FILE, | |
| state_file=state_file, | |
| exported_accounts_file=input_file, | |
| reset_retryable=not args.no_reset_retryable, | |
| reset_all_failed=args.reset_all_failed, | |
| prune_exported=True, | |
| backup=True, | |
| ) | |
| print( | |
| "Pool cleaned: " | |
| f"removed={result.removed_lines}, reset_states={result.reset_states}, " | |
| f"remaining={result.remaining_lines}, backup={result.backup_file}" | |
| ) | |
| return 0 | |
| def cmd_pool_clean(args) -> int: | |
| """Clean outlook token pool state/config.""" | |
| config = load_config(args.config) | |
| setup_logging(config, args.verbose) | |
| config_dir = Path(config.get("_config_dir", ".")) | |
| state_file = Path(args.state_file) if args.state_file else config_dir / "data" / "outlook_token_state.json" | |
| accounts_file = Path(args.accounts) if args.accounts else None | |
| result = clean_pool( | |
| config_file=args.config or DEFAULT_CONFIG_FILE, | |
| state_file=state_file, | |
| exported_accounts_file=accounts_file, | |
| reset_retryable=not args.no_reset_retryable, | |
| reset_all_failed=args.reset_all_failed, | |
| prune_exported=not args.no_prune_exported, | |
| backup=not args.no_backup, | |
| ) | |
| print("Pool clean summary:") | |
| print(f" Config: {result.config_file}") | |
| print(f" State: {result.state_file}") | |
| print(f" Backup: {result.backup_file}") | |
| print(f" Removed: {result.removed_lines}") | |
| print(f" Reset state: {result.reset_states}") | |
| print(f" Remaining: {result.remaining_lines}") | |
| return 0 | |
| def cmd_run(args) -> int: | |
| """Run the full pipeline.""" | |
| config = load_config(args.config) | |
| setup_logging(config, args.verbose) | |
| # Override workspace IDs from CLI | |
| if args.workspace_id: | |
| config.setdefault("workspace", {})["ids"] = args.workspace_id | |
| summary = run_full_pipeline( | |
| config=config, | |
| count=args.count, | |
| output_file=args.output, | |
| accounts_file=args.accounts, | |
| ) | |
| print(f"\n{'='*40}") | |
| print(f"Pipeline Summary:") | |
| print(f" Registered: {summary['registered']}") | |
| print(f" Joined: {summary['joined']}") | |
| print(f" K12 Refreshed: {summary['refreshed']}") | |
| print(f" Exported: {summary['exported']}") | |
| print(f" Accounts: {summary['accounts_file']}") | |
| print(f" Output: {summary.get('output_file', '')}") | |
| if args.check and summary.get("exported", 0) > 0: | |
| report = _check_exported_file(config, summary.get("output_file", ""), args) | |
| _print_check_report(report) | |
| if report["summary"]["failed"] > 0: | |
| return 2 | |
| return 0 if summary["registered"] > 0 else 1 | |
| def main(argv: list[str] | None = None) -> None: | |
| parser = argparse.ArgumentParser( | |
| prog="chatgpt-register", | |
| description="ChatGPT 账号注册 + K12 母号加入 + Sub2API JSON 导出", | |
| ) | |
| parser.add_argument( | |
| "--version", action="version", version=f"%(prog)s {__version__}" | |
| ) | |
| sub = parser.add_subparsers(dest="command", help="Available commands") | |
| # ── init ── | |
| p_init = sub.add_parser("init", help="Write default config.yaml") | |
| p_init.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_init.set_defaults(func=cmd_init) | |
| # ── cdk ── | |
| p_cdk = sub.add_parser("cdk", help="Generate an offline algorithmic CDK") | |
| p_cdk.add_argument("--secret", required=True, help="CDK_SECRET configured on the Worker") | |
| p_cdk.add_argument("--days", type=int, default=7, help="Valid days from today, default 7") | |
| p_cdk.add_argument("--expires", default="", help="Explicit expiry date YYYYMMDD") | |
| p_cdk.add_argument("--max-count", type=int, default=1, help="Max accounts per submitted job") | |
| p_cdk.add_argument("--nonce", default="", help="Optional nonce; auto-generated by default") | |
| p_cdk.set_defaults(func=cmd_cdk) | |
| # ── register ── | |
| p_reg = sub.add_parser("register", help="Register ChatGPT accounts") | |
| p_reg.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_reg.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | |
| p_reg.add_argument("--count", "-n", type=int, default=None, help="Number of accounts") | |
| p_reg.set_defaults(func=cmd_register) | |
| # ── join-workspace ── | |
| p_join = sub.add_parser("join-workspace", help="Join workspace") | |
| p_join.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_join.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | |
| p_join.add_argument("--workspace-id", action="append", default=None, help="Workspace UUID (repeatable)") | |
| p_join.add_argument("--input", "-i", default=None, help="Input accounts JSON") | |
| p_join.set_defaults(func=cmd_join_workspace) | |
| # ── login-team ── | |
| p_login = sub.add_parser("login-team", help="Re-login with Team space") | |
| p_login.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_login.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | |
| p_login.add_argument("--input", "-i", default=None, help="Input accounts JSON") | |
| p_login.set_defaults(func=cmd_login_team) | |
| # ── export ── | |
| p_export = sub.add_parser("export", help="Export sub2api JSON") | |
| p_export.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_export.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | |
| p_export.add_argument("--output", "-o", default=None, help="Output file path") | |
| p_export.add_argument("--input", "-i", default=None, help="Input accounts JSON") | |
| p_export.add_argument("--stdout", action="store_true", help="Print to stdout") | |
| p_export.add_argument("--check", action="store_true", help="Live-check exported sub2api JSON via /v1/me") | |
| p_export.add_argument("--timeout", type=float, default=15, help="Live-check request timeout seconds") | |
| p_export.add_argument("--retries", type=int, default=2, help="Live-check retry count for temporary errors") | |
| p_export.add_argument("--limit", type=int, default=None, help="Live-check only first N accounts") | |
| p_export.set_defaults(func=cmd_export) | |
| # ── check ── | |
| p_check = sub.add_parser("check", help="Live-check exported sub2api JSON") | |
| p_check.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_check.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | |
| p_check.add_argument("--input", "-i", default=None, help="Input sub2api JSON") | |
| p_check.add_argument("--timeout", type=float, default=15, help="Request timeout seconds") | |
| p_check.add_argument("--retries", type=int, default=2, help="Retry count for temporary errors") | |
| p_check.add_argument("--limit", type=int, default=None, help="Check only first N accounts") | |
| p_check.set_defaults(func=cmd_check) | |
| # ── export-cpa ── | |
| p_cpa = sub.add_parser("export-cpa", help="Export CPA/CLIProxyAPI per-account JSON") | |
| p_cpa.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_cpa.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | |
| p_cpa.add_argument("--input", "-i", default=None, help="Input accounts JSON") | |
| p_cpa.add_argument("--output-dir", default=None, help="Directory for per-account JSON files") | |
| p_cpa.add_argument("--zip", default=None, help="Optional ZIP output path") | |
| p_cpa.add_argument("--prune-config", action="store_true", help="Remove exported account mailboxes from config.yaml") | |
| p_cpa.add_argument("--no-reset-retryable", action="store_true", help="Do not clear retryable failed pool states") | |
| p_cpa.add_argument("--reset-all-failed", action="store_true", help="Clear all failed states, not only retryable failures") | |
| p_cpa.set_defaults(func=cmd_export_cpa) | |
| # ── pool-clean ── | |
| p_pool = sub.add_parser("pool-clean", help="Clean outlook pool config/state") | |
| p_pool.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_pool.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | |
| p_pool.add_argument("--accounts", default=None, help="Exported registered_accounts.json to prune from config") | |
| p_pool.add_argument("--state-file", default=None, help="Outlook token state JSON path") | |
| p_pool.add_argument("--no-reset-retryable", action="store_true", help="Do not clear retryable failed pool states") | |
| p_pool.add_argument("--reset-all-failed", action="store_true", help="Clear all failed states") | |
| p_pool.add_argument("--no-prune-exported", action="store_true", help="Do not remove exported mailboxes from config") | |
| p_pool.add_argument("--no-backup", action="store_true", help="Do not create config backup") | |
| p_pool.set_defaults(func=cmd_pool_clean) | |
| # ── run ── | |
| p_run = sub.add_parser("run", help="Full pipeline") | |
| p_run.add_argument("--config", "-c", default=None, help="Config file path") | |
| p_run.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | |
| p_run.add_argument("--count", "-n", type=int, default=None, help="Number of accounts") | |
| p_run.add_argument("--workspace-id", action="append", default=None, help="Workspace UUID (repeatable)") | |
| p_run.add_argument("--output", "-o", default=None, help="Output sub2api JSON file") | |
| p_run.add_argument("--accounts", default=None, help="Accounts store JSON file") | |
| p_run.add_argument("--check", action="store_true", help="Live-check exported sub2api JSON via /v1/me") | |
| p_run.add_argument("--timeout", type=float, default=15, help="Live-check request timeout seconds") | |
| p_run.add_argument("--retries", type=int, default=2, help="Live-check retry count for temporary errors") | |
| p_run.add_argument("--limit", type=int, default=None, help="Live-check only first N accounts") | |
| p_run.set_defaults(func=cmd_run) | |
| args = parser.parse_args(argv) | |
| if not args.command: | |
| parser.print_help() | |
| sys.exit(1) | |
| sys.exit(args.func(args) or 0) | |
| if __name__ == "__main__": | |
| main() | |