fierr's picture
Deploy chatgpt-register runner
7868d50 verified
Raw
History Blame Contribute Delete
19.8 kB
"""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()