Files
standalone-openai-pool-cli/main.py

433 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from pathlib import Path
from typing import Any, List
PROJECT_ROOT = Path(__file__).resolve().parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from openai_pool_orchestrator import TOKENS_DIR, __version__
from openai_pool_orchestrator.mail_providers import MultiMailRouter
from openai_pool_orchestrator.register import _write_text_atomic, run as register_run
try:
from .support import (
check_proxy,
get_pool_maintainer,
get_sub2api_maintainer,
init_config_from_example,
iter_token_files,
load_state,
load_sync_config,
login_sub2api_once,
normalize_sub2api_maintain_actions,
print_json,
print_status_block,
read_token_file,
save_runtime_proxy,
save_sub2api_credentials,
sub2api_actions_description,
sync_all_tokens_to_sub2api,
sync_token_to_sub2api,
upload_all_tokens_to_cpa,
)
except ImportError:
from support import (
check_proxy,
get_pool_maintainer,
get_sub2api_maintainer,
init_config_from_example,
iter_token_files,
load_state,
load_sync_config,
login_sub2api_once,
normalize_sub2api_maintain_actions,
print_json,
print_status_block,
read_token_file,
save_runtime_proxy,
save_sub2api_credentials,
sub2api_actions_description,
sync_all_tokens_to_sub2api,
sync_token_to_sub2api,
upload_all_tokens_to_cpa,
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="OpenAI Pool Orchestrator CLI")
parser.add_argument("--json", action="store_true", help="以 JSON 输出结果")
subparsers = parser.add_subparsers(dest="command")
register_parser = subparsers.add_parser("register", help="执行注册流程")
register_parser.add_argument("--proxy", default=None, help="代理地址,如 http://127.0.0.1:7897")
register_parser.add_argument("--once", action="store_true", help="只运行一次")
register_parser.add_argument("--sleep-min", type=int, default=5, help="循环模式最短等待秒数")
register_parser.add_argument("--sleep-max", type=int, default=30, help="循环模式最长等待秒数")
register_parser.set_defaults(handler=handle_register)
config_parser = subparsers.add_parser("config", help="配置管理")
config_subparsers = config_parser.add_subparsers(dest="config_command")
config_init = config_subparsers.add_parser("init", help="初始化配置文件到 data/")
config_init.set_defaults(handler=handle_config_init)
config_show = config_subparsers.add_parser("show", help="显示当前配置")
config_show.set_defaults(handler=handle_config_show)
config_proxy = config_subparsers.add_parser("proxy", help="保存运行代理")
config_proxy.add_argument("proxy", help="代理地址")
config_proxy.add_argument("--auto-register", action="store_true", help="同时启用池不足时自动注册")
config_proxy.set_defaults(handler=handle_config_proxy)
config_check_proxy = config_subparsers.add_parser("check-proxy", help="检测代理可用性")
config_check_proxy.add_argument("proxy", help="代理地址")
config_check_proxy.set_defaults(handler=handle_check_proxy)
config_sub2api = config_subparsers.add_parser("sub2api", help="保存 Sub2Api 配置并校验")
config_sub2api.add_argument("--base-url", required=True, help="Sub2Api 平台地址")
config_sub2api.add_argument("--bearer-token", default="", help="Bearer Token")
config_sub2api.add_argument("--email", default="", help="管理员邮箱")
config_sub2api.add_argument("--password", default="", help="管理员密码")
config_sub2api.add_argument("--account-name", default=None, help="默认账号名称")
config_sub2api.add_argument("--auto-sync", action="store_true", help="注册成功后自动同步 Sub2Api")
config_sub2api.set_defaults(handler=handle_config_sub2api)
config_login = config_subparsers.add_parser("sub2api-login", help="登录 Sub2Api 并保存 Bearer Token")
config_login.add_argument("--base-url", required=True, help="Sub2Api 平台地址")
config_login.add_argument("--email", required=True, help="管理员邮箱")
config_login.add_argument("--password", required=True, help="管理员密码")
config_login.set_defaults(handler=handle_sub2api_login)
tokens_parser = subparsers.add_parser("tokens", help="查看本地 token 文件")
tokens_parser.add_argument("--limit", type=int, default=20, help="最多显示数量")
tokens_parser.set_defaults(handler=handle_tokens)
cpa_parser = subparsers.add_parser("cpa", help="CPA 账号池命令")
cpa_subparsers = cpa_parser.add_subparsers(dest="cpa_command")
cpa_status = cpa_subparsers.add_parser("status", help="查看 CPA 池状态")
cpa_status.set_defaults(handler=handle_cpa_status)
cpa_check = cpa_subparsers.add_parser("check", help="测试 CPA 连接")
cpa_check.set_defaults(handler=handle_cpa_check)
cpa_maintain = cpa_subparsers.add_parser("maintain", help="执行 CPA 维护")
cpa_maintain.set_defaults(handler=handle_cpa_maintain)
cpa_upload = cpa_subparsers.add_parser("upload-all", help="上传本地全部 token 到 CPA")
cpa_upload.add_argument("--include-uploaded", action="store_true", help="包含已标记上传的 token")
cpa_upload.set_defaults(handler=handle_cpa_upload_all)
sub2api_parser = subparsers.add_parser("sub2api", help="Sub2Api 命令")
sub2api_subparsers = sub2api_parser.add_subparsers(dest="sub2api_command")
sub2api_status = sub2api_subparsers.add_parser("status", help="查看 Sub2Api 池状态")
sub2api_status.set_defaults(handler=handle_sub2api_status)
sub2api_check = sub2api_subparsers.add_parser("check", help="测试 Sub2Api 连接")
sub2api_check.set_defaults(handler=handle_sub2api_check)
sub2api_sync = sub2api_subparsers.add_parser("sync-all", help="同步本地全部 token 到 Sub2Api")
sub2api_sync.add_argument("--include-uploaded", action="store_true", help="包含已标记同步的 token")
sub2api_sync.set_defaults(handler=handle_sub2api_sync_all)
sub2api_sync_one = sub2api_subparsers.add_parser("sync-one", help="同步单个 token 到 Sub2Api")
sub2api_sync_one.add_argument("file", help="token 文件名或路径")
sub2api_sync_one.set_defaults(handler=handle_sub2api_sync_one)
sub2api_dedupe = sub2api_subparsers.add_parser("dedupe", help="Sub2Api 重复账号清理")
sub2api_dedupe.add_argument("--apply", action="store_true", help="实际执行删除,不仅预览")
sub2api_dedupe.set_defaults(handler=handle_sub2api_dedupe)
sub2api_handle = sub2api_subparsers.add_parser("handle-exception", help="处理异常账号")
sub2api_handle.add_argument("ids", nargs="*", type=int, help="指定账号 ID为空时处理全部异常账号")
sub2api_handle.add_argument("--no-delete", action="store_true", help="刷新后不删除仍异常账号")
sub2api_handle.set_defaults(handler=handle_sub2api_handle_exception)
sub2api_maintain = sub2api_subparsers.add_parser("maintain", help="执行 Sub2Api 综合维护")
sub2api_maintain.add_argument("--refresh-abnormal", action="store_true", help="仅显式开启异常测活")
sub2api_maintain.add_argument("--delete-abnormal", action="store_true", help="仅显式开启异常删除")
sub2api_maintain.add_argument("--dedupe-duplicate", action="store_true", help="仅显式开启重复清理")
sub2api_maintain.add_argument("--no-refresh-abnormal", action="store_true", help="关闭异常测活")
sub2api_maintain.add_argument("--no-delete-abnormal", action="store_true", help="关闭异常删除")
sub2api_maintain.add_argument("--no-dedupe-duplicate", action="store_true", help="关闭重复清理")
sub2api_maintain.set_defaults(handler=handle_sub2api_maintain)
stats_parser = subparsers.add_parser("stats", help="显示本地累计统计")
stats_parser.set_defaults(handler=handle_stats)
return parser
def _print_result(args: argparse.Namespace, result: Any) -> int:
if args.json:
print_json(result)
return 0
if isinstance(result, dict):
print_json(result)
return 0
print(result)
return 0
def handle_register(args: argparse.Namespace) -> dict[str, Any]:
cfg = load_sync_config()
os.makedirs(TOKENS_DIR, exist_ok=True)
sleep_min = max(1, args.sleep_min)
sleep_max = max(sleep_min, args.sleep_max)
proxy = args.proxy if args.proxy is not None else str(cfg.get("proxy") or "").strip() or None
mail_router = MultiMailRouter(cfg)
count = 0
runs: List[dict[str, Any]] = []
while True:
count += 1
print(f"\n[{time.strftime('%H:%M:%S')}] >>> 开始第 {count} 次注册流程 <<<")
try:
provider_name, provider = mail_router.next_provider()
print(f"[*] 本次使用邮箱提供商: {provider_name}")
token_json = register_run(
proxy,
mail_provider=provider,
proxy_pool_config={
"enabled": bool(cfg.get("proxy_pool_enabled", False)),
"api_url": cfg.get("proxy_pool_api_url", ""),
"auth_mode": cfg.get("proxy_pool_auth_mode", "query"),
"api_key": cfg.get("proxy_pool_api_key", ""),
"count": cfg.get("proxy_pool_count", 1),
"country": cfg.get("proxy_pool_country", "US"),
},
)
mail_router.report_success(provider_name)
except Exception as exc:
token_json = None
try:
mail_router.report_failure(provider_name)
except Exception:
pass
runs.append({"ok": False, "error": str(exc)})
print(f"[Error] 发生未捕获异常: {exc}")
if token_json:
token_data = json.loads(token_json)
email = str(token_data.get("email") or "unknown")
file_name = f"token_{email.replace('@', '_')}_{time.time_ns()}.json"
file_path = Path(TOKENS_DIR) / file_name
_write_text_atomic(str(file_path), token_json)
print(f"[*] 成功! Token 已保存至: {file_path}")
run_result: dict[str, Any] = {"ok": True, "file": file_name, "email": email}
cpa = get_pool_maintainer(cfg)
if cpa:
cpa_ok = cpa.upload_token(file_name, token_data, proxy=proxy or "")
run_result["cpa_uploaded"] = cpa_ok
print(f"[{'+' if cpa_ok else '-'}] CPA {'上传成功' if cpa_ok else '上传失败'}: {email}")
if cfg.get("auto_sync"):
try:
sync_result = sync_token_to_sub2api(file_path, cfg)
run_result["sub2api_sync"] = sync_result
if sync_result.get("ok"):
print(f"[+] Sub2Api 同步成功: {email}")
else:
print(f"[-] Sub2Api 同步失败: {email}")
except Exception as exc:
run_result["sub2api_sync"] = {"ok": False, "error": str(exc)}
print(f"[-] Sub2Api 同步异常: {exc}")
runs.append(run_result)
else:
if not runs or runs[-1].get("ok") is not False:
runs.append({"ok": False, "error": "本次注册失败"})
print("[-] 本次注册失败。")
if args.once:
break
wait_time = sleep_min if sleep_min == sleep_max else __import__("random").randint(sleep_min, sleep_max)
print(f"[*] 休息 {wait_time} 秒...")
time.sleep(wait_time)
return {"runs": runs}
def handle_config_init(args: argparse.Namespace) -> dict[str, Any]:
path = init_config_from_example(PROJECT_ROOT)
return {"ok": True, "config_file": str(path)}
def handle_config_show(args: argparse.Namespace) -> dict[str, Any]:
return load_sync_config()
def handle_config_proxy(args: argparse.Namespace) -> dict[str, Any]:
cfg = save_runtime_proxy(args.proxy, auto_register=args.auto_register)
return {"ok": True, "proxy": cfg.get("proxy", ""), "auto_register": cfg.get("auto_register", False)}
def handle_check_proxy(args: argparse.Namespace) -> dict[str, Any]:
return check_proxy(args.proxy)
def handle_config_sub2api(args: argparse.Namespace) -> dict[str, Any]:
cfg = save_sub2api_credentials(
base_url=args.base_url,
bearer_token=args.bearer_token,
email=args.email,
password=args.password,
account_name=args.account_name,
auto_sync=args.auto_sync,
)
return {"ok": True, "base_url": cfg.get("base_url", ""), "auto_sync": cfg.get("auto_sync", False)}
def handle_sub2api_login(args: argparse.Namespace) -> dict[str, Any]:
return login_sub2api_once(args.base_url, args.email, args.password)
def handle_tokens(args: argparse.Namespace) -> dict[str, Any]:
items = []
for index, path in enumerate(iter_token_files()):
if index >= max(1, args.limit):
break
try:
token_data = read_token_file(path)
items.append(
{
"file": path.name,
"email": token_data.get("email", ""),
"uploaded_platforms": token_data.get("uploaded_platforms", []),
}
)
except Exception as exc:
items.append({"file": path.name, "error": str(exc)})
return {"total_shown": len(items), "items": items}
def handle_cpa_status(args: argparse.Namespace) -> dict[str, Any]:
maintainer = require_pool_maintainer()
return maintainer.get_pool_status()
def handle_cpa_check(args: argparse.Namespace) -> dict[str, Any]:
maintainer = require_pool_maintainer()
return maintainer.test_connection()
def handle_cpa_maintain(args: argparse.Namespace) -> dict[str, Any]:
maintainer = require_pool_maintainer()
return maintainer.probe_and_clean_sync()
def handle_cpa_upload_all(args: argparse.Namespace) -> dict[str, Any]:
return upload_all_tokens_to_cpa(skip_uploaded=not args.include_uploaded)
def handle_sub2api_status(args: argparse.Namespace) -> dict[str, Any]:
maintainer = require_sub2api_maintainer()
status = maintainer.get_pool_status()
status["dashboard"] = maintainer.get_dashboard_stats()
return status
def handle_sub2api_check(args: argparse.Namespace) -> dict[str, Any]:
maintainer = require_sub2api_maintainer()
return maintainer.test_connection()
def handle_sub2api_sync_all(args: argparse.Namespace) -> dict[str, Any]:
return sync_all_tokens_to_sub2api(skip_uploaded=not args.include_uploaded)
def handle_sub2api_sync_one(args: argparse.Namespace) -> dict[str, Any]:
raw_path = Path(args.file)
file_path = raw_path if raw_path.is_absolute() else (Path(TOKENS_DIR) / args.file)
if not file_path.exists():
raise ValueError(f"token 文件不存在: {args.file}")
return sync_token_to_sub2api(file_path)
def handle_sub2api_dedupe(args: argparse.Namespace) -> dict[str, Any]:
maintainer = require_sub2api_maintainer()
return maintainer.dedupe_duplicate_accounts(dry_run=not args.apply)
def handle_sub2api_handle_exception(args: argparse.Namespace) -> dict[str, Any]:
maintainer = require_sub2api_maintainer()
return maintainer.handle_exception_accounts(account_ids=args.ids, delete_unresolved=not args.no_delete)
def handle_sub2api_maintain(args: argparse.Namespace) -> dict[str, Any]:
maintainer = require_sub2api_maintainer()
cfg = load_sync_config()
actions = normalize_sub2api_maintain_actions(cfg.get("sub2api_maintain_actions"))
overrides = {
"refresh_abnormal_accounts": (args.refresh_abnormal, args.no_refresh_abnormal),
"delete_abnormal_accounts": (args.delete_abnormal, args.no_delete_abnormal),
"dedupe_duplicate_accounts": (args.dedupe_duplicate, args.no_dedupe_duplicate),
}
for key, (enable, disable) in overrides.items():
if enable:
actions[key] = True
if disable:
actions[key] = False
result = maintainer.probe_and_clean_sync(actions=actions)
result["actions_text"] = sub2api_actions_description(actions)
return result
def handle_stats(args: argparse.Namespace) -> dict[str, Any]:
return load_state()
def require_pool_maintainer():
maintainer = get_pool_maintainer()
if not maintainer:
raise ValueError("CPA 未配置,请先在 data/sync_config.json 中填写 cpa_base_url 和 cpa_token")
return maintainer
def require_sub2api_maintainer():
maintainer = get_sub2api_maintainer()
if not maintainer:
raise ValueError("Sub2Api 未配置,请先在 data/sync_config.json 中填写 base_url 与认证信息")
return maintainer
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if not getattr(args, "command", None):
parser.print_help()
return 0
try:
result = args.handler(args)
except Exception as exc:
if getattr(args, "json", False):
print_json({"ok": False, "error": str(exc)})
else:
print(f"错误: {exc}", file=sys.stderr)
return 1
if not getattr(args, "json", False) and args.command in {"cpa", "sub2api"} and isinstance(result, dict):
if args.command == "cpa" and args.cpa_command == "status":
print_status_block(f"OpenAI Pool Orchestrator v{__version__} - CPA 状态", result)
return 0
if args.command == "sub2api" and args.sub2api_command == "status":
print_status_block(f"OpenAI Pool Orchestrator v{__version__} - Sub2Api 状态", result)
return 0
return _print_result(args, result)
if __name__ == "__main__":
raise SystemExit(main())