feat: save registered accounts to postgres

This commit is contained in:
mmc
2026-03-19 14:32:17 +08:00
parent 6a250fe6a1
commit 2904e43b1f
11 changed files with 235 additions and 4 deletions

View File

@@ -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": {

View File

@@ -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

View File

@@ -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`

73
account_store.py Normal file
View File

@@ -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"]}

View File

@@ -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"
],

31
main.py
View File

@@ -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"

View File

@@ -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)

View File

@@ -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")

View File

@@ -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*"]

View File

@@ -1,3 +1,4 @@
curl-cffi>=0.6
aiohttp>=3.9
requests>=2.31
psycopg[binary]>=3.3

View File

@@ -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"]