423 lines
17 KiB
Python
423 lines
17 KiB
Python
#!/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.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
|
||
|
||
count = 0
|
||
runs: List[dict[str, Any]] = []
|
||
while True:
|
||
count += 1
|
||
print(f"\n[{time.strftime('%H:%M:%S')}] >>> 开始第 {count} 次注册流程 <<<")
|
||
try:
|
||
token_json = register_run(
|
||
proxy,
|
||
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"),
|
||
},
|
||
)
|
||
except Exception as exc:
|
||
token_json = None
|
||
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())
|