From 2904e43b1f6b2f36e42add19aedf17ffeec0a3f0 Mon Sep 17 00:00:00 2001 From: mmc Date: Thu, 19 Mar 2026 14:32:17 +0800 Subject: [PATCH] feat: save registered accounts to postgres --- CONFIG_GUIDE.md | 43 +++++++++++++ Dockerfile | 2 +- README.md | 15 +++++ account_store.py | 73 ++++++++++++++++++++++ config/sync_config.example.json | 8 +++ main.py | 31 +++++++++ openai_pool_orchestrator/mail_providers.py | 12 ++++ openai_pool_orchestrator/register.py | 32 +++++++++- pyproject.toml | 3 +- requirements.txt | 1 + support.py | 19 ++++++ 11 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 account_store.py diff --git a/CONFIG_GUIDE.md b/CONFIG_GUIDE.md index 1a97282..09baa80 100644 --- a/CONFIG_GUIDE.md +++ b/CONFIG_GUIDE.md @@ -397,6 +397,41 @@ ocxxxxxxx@cursors.online - `proxy_pool_auth_mode` 一般是 `header` 或 `query` - `proxy_pool_country` 常见填 `US` +## 23. PostgreSQL 入库配置 + +如果你希望注册成功后,把账号资料写入 PostgreSQL 的 `registered_accounts` 表,可以配置以下字段: + +```json +"db_enabled": true, +"db_host": "150.158.105.6", +"db_port": 54321, +"db_name": "mail_accounts_db", +"db_user": "postgres", +"db_password": "your_password", +"db_table": "registered_accounts", +"db_source": "standalone_cli" +``` + +说明: + +- `db_enabled`:是否启用入库 +- `db_name`:当前你这边实际表所在库是 `mail_accounts_db` +- `db_table`:目标表名 +- `db_source`:写入表中 `source` 字段的值 + +当前实现会尽量写入这些字段: + +- `email` +- `chatgpt_password` +- `mail_password` +- `oauth_status` +- `mail_token` +- `name` +- `birthdate` +- `source` + +拿到什么就写什么,缺失字段会写空值或 `NULL`。 + ## 推荐的最小可用配置 如果你要先跑注册,再决定同步哪个平台,可以先这样: @@ -406,6 +441,14 @@ ocxxxxxxx@cursors.online "proxy": "http://127.0.0.1:17891", "auto_register": false, "auto_register_max_per_loop": 1, + "db_enabled": false, + "db_host": "150.158.105.6", + "db_port": 54321, + "db_name": "mail_accounts_db", + "db_user": "postgres", + "db_password": "", + "db_table": "registered_accounts", + "db_source": "standalone_cli", "mail_providers": ["mailtm"], "mail_provider_configs": { "mailtm": { diff --git a/Dockerfile b/Dockerfile index 5453626..743a909 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN apt-get update && \ WORKDIR /app COPY requirements.txt pyproject.toml README.md ./ -COPY main.py run.py support.py ./ +COPY main.py run.py support.py account_store.py ./ COPY openai_pool_orchestrator ./openai_pool_orchestrator COPY config ./config diff --git a/README.md b/README.md index c648fad..747fca5 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ python3 /root/standalone_cli/run.py config setup `config init` 只是初始化配置文件;`config setup` 才是交互式配置引导。 +`config setup` 现在也支持交互式填写 PostgreSQL 入库配置。 + 初始化后请编辑: - `/root/standalone_cli/data/sync_config.json` @@ -65,6 +67,19 @@ python3 /root/standalone_cli/run.py config setup - `bearer_token` 或 `email` + `password` - 邮箱提供商配置 `mail_provider_configs` +如果你希望注册成功后把邮箱、GPT 密码、邮箱密码、姓名、生日等信息写入 PostgreSQL,还需要填写: + +- `db_enabled` +- `db_host` +- `db_port` +- `db_name` +- `db_user` +- `db_password` +- `db_table` +- `db_source` + +当前你这边实际使用的数据库名是:`mail_accounts_db` + 当前这台机器实测可用的代理是: - `http://127.0.0.1:17891` diff --git a/account_store.py b/account_store.py new file mode 100644 index 0000000..f406403 --- /dev/null +++ b/account_store.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import Any, Dict, Optional + +import psycopg + + +def is_db_enabled(cfg: Dict[str, Any]) -> bool: + return bool(cfg.get("db_enabled", False)) + + +def _dsn(cfg: Dict[str, Any]) -> str: + return ( + f"host={cfg.get('db_host', '')} " + f"port={int(cfg.get('db_port', 5432) or 5432)} " + f"dbname={cfg.get('db_name', 'postgres')} " + f"user={cfg.get('db_user', '')} " + f"password={cfg.get('db_password', '')} " + "connect_timeout=10" + ) + + +def save_registered_account(cfg: Dict[str, Any], record: Dict[str, Any]) -> Dict[str, Any]: + if not is_db_enabled(cfg): + return {"ok": False, "skipped": True, "reason": "db_disabled"} + + table = str(cfg.get("db_table", "registered_accounts") or "registered_accounts").strip() + source = str(cfg.get("db_source", "standalone_cli") or "standalone_cli").strip() + payload = { + "email": str(record.get("email") or "").strip() or None, + "chatgpt_password": str(record.get("chatgpt_password") or "").strip() or None, + "mail_password": str(record.get("mail_password") or "").strip() or None, + "oauth_status": str(record.get("oauth_status") or "oauth=ok").strip() or "oauth=ok", + "mail_token": str(record.get("mail_token") or "").strip() or None, + "name": str(record.get("name") or "").strip() or None, + "birthdate": str(record.get("birthdate") or "").strip() or None, + "source": str(record.get("source") or source).strip() or source, + } + + if not payload["email"]: + return {"ok": False, "error": "missing email"} + + sql = f""" + INSERT INTO {table} ( + email, + chatgpt_password, + mail_password, + oauth_status, + created_at, + updated_at, + mail_token, + name, + birthdate, + source + ) VALUES ( + %(email)s, + %(chatgpt_password)s, + %(mail_password)s, + %(oauth_status)s, + NOW(), + NOW(), + %(mail_token)s, + %(name)s, + %(birthdate)s, + %(source)s + ) + """ + + with psycopg.connect(_dsn(cfg)) as conn: + with conn.cursor() as cur: + cur.execute(sql, payload) + conn.commit() + return {"ok": True, "skipped": False, "table": table, "email": payload["email"]} diff --git a/config/sync_config.example.json b/config/sync_config.example.json index 5916d00..0140bb2 100755 --- a/config/sync_config.example.json +++ b/config/sync_config.example.json @@ -2,6 +2,14 @@ "proxy": "http://127.0.0.1:17891", "auto_register": false, "auto_register_max_per_loop": 1, + "db_enabled": false, + "db_host": "150.158.105.6", + "db_port": 54321, + "db_name": "mail_accounts_db", + "db_user": "postgres", + "db_password": "", + "db_table": "registered_accounts", + "db_source": "standalone_cli", "mail_providers": [ "mailtm" ], diff --git a/main.py b/main.py index ad80b0b..a2e2150 100644 --- a/main.py +++ b/main.py @@ -17,6 +17,7 @@ if str(PROJECT_ROOT) not in sys.path: 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 +from account_store import save_registered_account try: from .support import ( @@ -231,6 +232,27 @@ def _perform_registration_once(cfg: dict[str, Any], proxy: Optional[str]) -> dic print(f"[*] 成功! Token 已保存至: {file_path}") run_result: dict[str, Any] = {"ok": True, "file": file_name, "email": email} + if cfg.get("db_enabled"): + try: + db_result = save_registered_account( + cfg, + { + "email": token_data.get("email", ""), + "chatgpt_password": token_data.get("account_password", ""), + "mail_password": token_data.get("mail_password", ""), + "oauth_status": "oauth=ok", + "mail_token": token_data.get("mail_token", ""), + "name": token_data.get("name", ""), + "birthdate": token_data.get("birthdate", ""), + "source": cfg.get("db_source", "standalone_cli"), + }, + ) + run_result["db_saved"] = bool(db_result.get("ok", False)) + print(f"[{'+' if db_result.get('ok') else '-'}] 数据库{'写入成功' if db_result.get('ok') else '写入失败'}: {email}") + except Exception as exc: + run_result["db_saved"] = False + run_result["db_error"] = str(exc) + print(f"[-] 数据库写入异常: {exc}") cpa = get_pool_maintainer(cfg) if cpa: cpa_ok = cpa.upload_token(file_name, token_data, proxy="") @@ -479,6 +501,15 @@ def handle_config_setup(args: argparse.Namespace) -> dict[str, Any]: cfg["auto_register"] = _prompt_bool("2) 池不足时自动注册", bool(cfg.get("auto_register", False))) cfg["auto_register_max_per_loop"] = _prompt_int(" 每轮最多自动补号数量", int(cfg.get("auto_register_max_per_loop", 1) or 1)) print(" 提示: 这个值越大,补号越快,但也会更激进。一般先用 1 或 2。") + cfg["db_enabled"] = _prompt_bool(" 是否启用 PostgreSQL 注册信息入库", bool(cfg.get("db_enabled", False))) + if cfg["db_enabled"]: + cfg["db_host"] = _prompt_text(" DB 主机", str(cfg.get("db_host") or "150.158.105.6")) + cfg["db_port"] = _prompt_int(" DB 端口", int(cfg.get("db_port", 54321) or 54321)) + cfg["db_name"] = _prompt_text(" DB 名称", str(cfg.get("db_name") or "mail_accounts_db")) + cfg["db_user"] = _prompt_text(" DB 用户名", str(cfg.get("db_user") or "postgres")) + cfg["db_password"] = _prompt_text(" DB 密码", str(cfg.get("db_password") or "")) + cfg["db_table"] = _prompt_text(" DB 表名", str(cfg.get("db_table") or "registered_accounts")) + cfg["db_source"] = _prompt_text(" source 字段值", str(cfg.get("db_source") or "standalone_cli")) provider_options = ["mailtm", "duckmail", "moemail", "cloudflare_temp_email"] current_provider = str((cfg.get("mail_providers") or ["mailtm"])[0]).strip().lower() or "mailtm" diff --git a/openai_pool_orchestrator/mail_providers.py b/openai_pool_orchestrator/mail_providers.py index 128797f..25f4971 100755 --- a/openai_pool_orchestrator/mail_providers.py +++ b/openai_pool_orchestrator/mail_providers.py @@ -155,6 +155,7 @@ class MailProvider(ABC): class MailTmProvider(MailProvider): def __init__(self, api_base: str = "https://api.mail.tm"): self.api_base = api_base.rstrip("/") + self.last_mailbox_info: Dict[str, str] = {} def _headers(self, token: str = "", use_json: bool = False) -> Dict[str, str]: h: Dict[str, str] = {"Accept": "application/json"} @@ -215,6 +216,11 @@ class MailTmProvider(MailProvider): if token_resp.status_code == 200: token = str(token_resp.json().get("token") or "").strip() if token: + self.last_mailbox_info = { + "email": email, + "mail_password": password, + "mail_token": token, + } return email, token return "", "" @@ -402,6 +408,7 @@ class DuckMailProvider(MailProvider): self.api_base = api_base.rstrip("/") self.bearer_token = bearer_token self.domain = str(domain).strip() + self.last_mailbox_info: Dict[str, str] = {} def _auth_headers(self, token: str = "") -> Dict[str, str]: h: Dict[str, str] = {"Accept": "application/json"} @@ -455,6 +462,11 @@ class DuckMailProvider(MailProvider): if token_resp.status_code == 200: mail_token = token_resp.json().get("token") if mail_token: + self.last_mailbox_info = { + "email": email, + "mail_password": password, + "mail_token": str(mail_token), + } return email, str(mail_token) except Exception as exc: logger.warning("DuckMail 创建邮箱失败: %s", exc) diff --git a/openai_pool_orchestrator/register.py b/openai_pool_orchestrator/register.py index 3d587c2..288c36d 100755 --- a/openai_pool_orchestrator/register.py +++ b/openai_pool_orchestrator/register.py @@ -731,7 +731,14 @@ def _post_form( ) from exc -def _build_token_result(token_payload: Dict[str, Any], account_password: str = "") -> str: +def _build_token_result( + token_payload: Dict[str, Any], + account_password: str = "", + mail_password: str = "", + mail_token: str = "", + name: str = "", + birthdate: str = "", +) -> str: access_token = str(token_payload.get("access_token") or "").strip() refresh_token = str(token_payload.get("refresh_token") or "").strip() id_token = str(token_payload.get("id_token") or "").strip() @@ -773,6 +780,14 @@ def _build_token_result(token_payload: Dict[str, Any], account_password: str = " } if account_password: config["account_password"] = account_password + if mail_password: + config["mail_password"] = mail_password + if mail_token: + config["mail_token"] = mail_token + if name: + config["name"] = name + if birthdate: + config["birthdate"] = birthdate return json.dumps(config, ensure_ascii=False, separators=(",", ":")) @@ -1321,6 +1336,8 @@ def run( return None # ------- 步骤2:创建临时邮箱 ------- + mailbox_password = "" + mailbox_token = "" if mail_provider is not None: emitter.info("正在创建临时邮箱...", step="create_email") try: @@ -1330,6 +1347,9 @@ def run( ) except TypeError: email, dev_token = mail_provider.create_mailbox(proxy="") + mailbox_info = getattr(mail_provider, "last_mailbox_info", {}) or {} + mailbox_password = str(mailbox_info.get("mail_password") or "").strip() + mailbox_token = str(mailbox_info.get("mail_token") or dev_token or "").strip() else: emitter.info("正在创建 Mail.tm 临时邮箱...", step="create_email") email, dev_token = get_email_and_token( @@ -1337,6 +1357,7 @@ def run( emitter, proxy_selector=None, ) + mailbox_token = str(dev_token or "").strip() if not email or not dev_token: emitter.error("临时邮箱创建失败", step="create_email") return None @@ -2012,7 +2033,14 @@ def run( emitter.success("Token 获取成功!", step="get_token") try: s.close() except: pass - return _build_token_result(_token_json, account_password=account_password) + return _build_token_result( + _token_json, + account_password=account_password, + mail_password=mailbox_password, + mail_token=mailbox_token, + name=_rand_name, + birthdate=_rand_bday, + ) except Exception as e: emitter.error(f"运行时发生错误: {e}", step="runtime") diff --git a/pyproject.toml b/pyproject.toml index c057e79..8411958 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "curl-cffi>=0.6", "aiohttp>=3.9", "requests>=2.31", + "psycopg[binary]>=3.3", ] [project.scripts] @@ -30,7 +31,7 @@ requires = ["setuptools>=68.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -py-modules = ["main", "run", "support"] +py-modules = ["main", "run", "support", "account_store"] [tool.setuptools.packages.find] include = ["openai_pool_orchestrator*"] diff --git a/requirements.txt b/requirements.txt index f61879e..fc00284 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ curl-cffi>=0.6 aiohttp>=3.9 requests>=2.31 +psycopg[binary]>=3.3 diff --git a/support.py b/support.py index c0a843a..f1c8dd9 100644 --- a/support.py +++ b/support.py @@ -43,6 +43,14 @@ DEFAULT_CONFIG: Dict[str, Any] = { "proxy": "", "auto_register": False, "auto_register_max_per_loop": 1, + "db_enabled": False, + "db_host": "150.158.105.6", + "db_port": 54321, + "db_name": "postgres", + "db_user": "postgres", + "db_password": "", + "db_table": "registered_accounts", + "db_source": "standalone_cli", "proxy_pool_enabled": True, "proxy_pool_api_url": "https://zenproxy.top/api/fetch", "proxy_pool_auth_mode": "query", @@ -179,6 +187,17 @@ def normalize_config(cfg: Dict[str, Any]) -> Dict[str, Any]: cfg["auto_register_max_per_loop"] = max(1, min(int(cfg.get("auto_register_max_per_loop", 1)), 20)) except (TypeError, ValueError): cfg["auto_register_max_per_loop"] = 1 + cfg["db_enabled"] = _as_bool(cfg.get("db_enabled", False), default=False) + cfg["db_host"] = str(cfg.get("db_host", DEFAULT_CONFIG["db_host"]) or DEFAULT_CONFIG["db_host"]).strip() + try: + cfg["db_port"] = max(1, int(cfg.get("db_port", DEFAULT_CONFIG["db_port"]) or DEFAULT_CONFIG["db_port"])) + except (TypeError, ValueError): + cfg["db_port"] = DEFAULT_CONFIG["db_port"] + cfg["db_name"] = str(cfg.get("db_name", DEFAULT_CONFIG["db_name"]) or DEFAULT_CONFIG["db_name"]).strip() + cfg["db_user"] = str(cfg.get("db_user", DEFAULT_CONFIG["db_user"]) or DEFAULT_CONFIG["db_user"]).strip() + cfg["db_password"] = str(cfg.get("db_password", DEFAULT_CONFIG["db_password"]) or DEFAULT_CONFIG["db_password"]).strip() + cfg["db_table"] = str(cfg.get("db_table", DEFAULT_CONFIG["db_table"]) or DEFAULT_CONFIG["db_table"]).strip() + cfg["db_source"] = str(cfg.get("db_source", DEFAULT_CONFIG["db_source"]) or DEFAULT_CONFIG["db_source"]).strip() cfg["proxy_pool_enabled"] = _as_bool(cfg.get("proxy_pool_enabled", True), default=True) proxy_pool_api_url = str(cfg.get("proxy_pool_api_url", DEFAULT_CONFIG["proxy_pool_api_url"]) or "").strip() cfg["proxy_pool_api_url"] = proxy_pool_api_url or DEFAULT_CONFIG["proxy_pool_api_url"]