|
@@ -1,4 +1,4 @@
|
|
|
-"""本地 Web 控制台:网页配置 + 一键全自动注册→付款→上传 CPA。"""
|
|
|
|
|
|
|
+"""本地 Web 控制台:SSO 注册、账号库与任务 API。"""
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import json
|
|
import json
|
|
@@ -7,8 +7,9 @@ import threading
|
|
|
import time
|
|
import time
|
|
|
from dataclasses import asdict
|
|
from dataclasses import asdict
|
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
|
|
|
-from chatgpt_flow import FullRunContext, run_full
|
|
|
|
|
|
|
+from chatgpt_flow import FullRunContext, run_sso_batch
|
|
|
from config import AppConfig
|
|
from config import AppConfig
|
|
|
from cpa_uploader import build_cpa_auth_payload
|
|
from cpa_uploader import build_cpa_auth_payload
|
|
|
from recheck import recheck_account
|
|
from recheck import recheck_account
|
|
@@ -27,6 +28,13 @@ from task_runner import get_runner, make_task_id
|
|
|
|
|
|
|
|
HOST = "127.0.0.1" # main() 会读 cfg.api_host 覆盖
|
|
HOST = "127.0.0.1" # main() 会读 cfg.api_host 覆盖
|
|
|
PORT = 7791
|
|
PORT = 7791
|
|
|
|
|
+BASE_DIR = Path(__file__).resolve().parent
|
|
|
|
|
+UI_DIR = BASE_DIR / "ui"
|
|
|
|
|
+STATIC_TYPES = {
|
|
|
|
|
+ ".css": "text/css; charset=utf-8",
|
|
|
|
|
+ ".js": "application/javascript; charset=utf-8",
|
|
|
|
|
+ ".html": "text/html; charset=utf-8",
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
|
|
|
class JobManager:
|
|
class JobManager:
|
|
@@ -50,7 +58,9 @@ class JobManager:
|
|
|
# stage 也写到日志,便于复盘
|
|
# stage 也写到日志,便于复盘
|
|
|
self._log(f"[STAGE] {name}")
|
|
self._log(f"[STAGE] {name}")
|
|
|
|
|
|
|
|
- def start(self, cfg: AppConfig) -> str:
|
|
|
|
|
|
|
+ def start_sso(self, account_count: int, sso_mail_domain: str,
|
|
|
|
|
+ cpa_url: str, cpa_management_key: str,
|
|
|
|
|
+ headless: bool, proxy_url: str) -> str:
|
|
|
with self.lock:
|
|
with self.lock:
|
|
|
if self.thread and self.thread.is_alive():
|
|
if self.thread and self.thread.is_alive():
|
|
|
return "已有任务在运行"
|
|
return "已有任务在运行"
|
|
@@ -61,10 +71,19 @@ class JobManager:
|
|
|
|
|
|
|
|
def runner():
|
|
def runner():
|
|
|
try:
|
|
try:
|
|
|
- self.full_ctx = run_full(cfg, log=self._log, on_stage=self._on_stage)
|
|
|
|
|
|
|
+ self.full_ctx = run_sso_batch(
|
|
|
|
|
+ account_count=account_count,
|
|
|
|
|
+ sso_mail_domain=sso_mail_domain,
|
|
|
|
|
+ cpa_url=cpa_url,
|
|
|
|
|
+ cpa_management_key=cpa_management_key,
|
|
|
|
|
+ headless=headless,
|
|
|
|
|
+ proxy_url=proxy_url,
|
|
|
|
|
+ log=self._log,
|
|
|
|
|
+ on_stage=self._on_stage,
|
|
|
|
|
+ )
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
import traceback
|
|
import traceback
|
|
|
- self._log(f"[server] 任务异常: {exc!r}")
|
|
|
|
|
|
|
+ self._log(f"[server] SSO 任务异常: {exc!r}")
|
|
|
self._log(traceback.format_exc())
|
|
self._log(traceback.format_exc())
|
|
|
|
|
|
|
|
self.thread = threading.Thread(target=runner, daemon=True)
|
|
self.thread = threading.Thread(target=runner, daemon=True)
|
|
@@ -102,565 +121,6 @@ class JobManager:
|
|
|
JOB = JobManager()
|
|
JOB = JobManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
-INDEX_HTML = r"""<!doctype html>
|
|
|
|
|
-<html lang="zh-CN">
|
|
|
|
|
-<head>
|
|
|
|
|
-<meta charset="utf-8" />
|
|
|
|
|
-<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
|
|
|
-<title>ChatGPT Plus 全自动注册</title>
|
|
|
|
|
-<style>
|
|
|
|
|
- :root { color-scheme: light; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; }
|
|
|
|
|
- body { margin:0; background:#f5f5f7; color:#111; }
|
|
|
|
|
- .wrap { max-width: 980px; margin: 24px auto; padding: 0 16px; }
|
|
|
|
|
- .card { background:#fff; border:1px solid #ddd; border-radius:18px; padding:22px; box-shadow:0 14px 40px rgba(0,0,0,.06); margin-bottom:18px; }
|
|
|
|
|
- h1 { margin: 0 0 6px; font-size: 22px; }
|
|
|
|
|
- h2 { margin: 0 0 10px; font-size: 16px; }
|
|
|
|
|
- p, li { color:#666; line-height:1.6; }
|
|
|
|
|
- label { display:block; margin:14px 0 6px; font-weight:600; }
|
|
|
|
|
- input, select { width:100%; box-sizing:border-box; border:1px solid #ccc; border-radius:10px; padding:9px 10px; font:inherit; background:#fff; }
|
|
|
|
|
- .grid { display:grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
|
|
|
- .grid-3 { display:grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
|
|
|
|
|
- .row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:14px; }
|
|
|
|
|
- button { border:0; border-radius:12px; background:#111; color:#fff; padding:10px 16px; font-weight:700; cursor:pointer; }
|
|
|
|
|
- button.secondary { background:#e9e9ec; color:#111; }
|
|
|
|
|
- button:disabled { opacity:.55; cursor:not-allowed; }
|
|
|
|
|
- .muted { color:#777; font-size:13px; }
|
|
|
|
|
- .chip { display:inline-block; padding:3px 10px; border-radius:999px; font-size:12px; background:#eef; color:#225; }
|
|
|
|
|
- .chip.green { background:#e6f7ec; color:#0f5f22; }
|
|
|
|
|
- .chip.red { background:#fde7e7; color:#a40000; }
|
|
|
|
|
- .chip.gray { background:#eee; color:#444; }
|
|
|
|
|
- .chip.blue { background:#e7f0ff; color:#1d4ed8; }
|
|
|
|
|
- pre.log { height:340px; overflow:auto; background:#0b0b10; color:#d6d6dc; padding:12px; border-radius:12px; font-size:12px; line-height:1.5; white-space:pre-wrap; word-break:break-all; }
|
|
|
|
|
- table { width:100%; border-collapse: collapse; font-size:13px; table-layout: fixed; }
|
|
|
|
|
- th, td { padding:8px 10px; border-bottom:1px solid #eee; text-align:left; vertical-align:top; word-break: break-word; }
|
|
|
|
|
- th { background:#fafafa; font-weight:600; color:#333; }
|
|
|
|
|
- /* 已注册账号库表 */
|
|
|
|
|
- #dbTable th.col-email, #dbTable td.col-email { width: 24%; }
|
|
|
|
|
- #dbTable th.col-plan, #dbTable td.col-plan { width: 60px; }
|
|
|
|
|
- #dbTable th.col-stat, #dbTable td.col-stat { width: 110px; }
|
|
|
|
|
- #dbTable th.col-cpa, #dbTable td.col-cpa { width: 22%; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; }
|
|
|
|
|
- #dbTable th.col-time, #dbTable td.col-time { width: 130px; white-space: nowrap; }
|
|
|
|
|
- #dbTable th.col-err, #dbTable td.col-err { width: 14%; color:#a40000; }
|
|
|
|
|
- #dbTable th.col-act, #dbTable td.col-act { width: 150px; white-space: nowrap; text-align: right; }
|
|
|
|
|
- #dbTable td.col-act .action-btn { display:inline-block; padding:4px 10px; border-radius:8px; font-weight:600; font-size:12px; text-decoration:none; white-space:nowrap; margin-left:6px; cursor:pointer; border:0; }
|
|
|
|
|
- #dbTable td.col-act .action-btn.detail { background:#e9e9ec; color:#111; }
|
|
|
|
|
- #dbTable td.col-act .action-btn.download { background:#e6f7ec; color:#0f5f22; }
|
|
|
|
|
- #dbTable td.col-act .action-btn.recheck { background:#fff4d6; color:#7a4f00; }
|
|
|
|
|
- #dbTable td.col-act .action-btn.retry-pay { background:#e7f0ff; color:#1d4ed8; }
|
|
|
|
|
- #dbTable td.col-act .action-btn:disabled { opacity:.6; cursor:not-allowed; }
|
|
|
|
|
- #dbTable td.col-email code { font-size: 12px; word-break: break-all; }
|
|
|
|
|
- .stage-box { padding:10px 14px; border-radius:12px; background:#fffaf0; border:1px solid #ffe2a8; color:#7a4f00; font-size:13px; min-height: 22px; }
|
|
|
|
|
- .pager { display:flex; align-items:center; gap:8px; margin-top:12px; flex-wrap:wrap; }
|
|
|
|
|
- .pager button { padding:6px 14px; border-radius:8px; font-size:13px; min-width:36px; }
|
|
|
|
|
- .pager button.active { background:#111; color:#fff; }
|
|
|
|
|
- .pager button:not(.active) { background:#e9e9ec; color:#111; }
|
|
|
|
|
- .pager .info { font-size:13px; color:#777; }
|
|
|
|
|
-</style>
|
|
|
|
|
-</head>
|
|
|
|
|
-<body>
|
|
|
|
|
-<div class="wrap">
|
|
|
|
|
-
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h1>ChatGPT Plus 全自动注册 + CPA 上传</h1>
|
|
|
|
|
- <div class="muted">流程:a4sky 邮箱注册 → 拿 Plus 长链 → PayPal 创建账号付款 → 校验 plan=plus → 上传 CPA。手机号统一 +15822201173。</div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h2>配置</h2>
|
|
|
|
|
-
|
|
|
|
|
- <div class="grid">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>账号数量</label>
|
|
|
|
|
- <input id="cfg_account_count" type="number" min="1" value="1" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>浏览器模式</label>
|
|
|
|
|
- <select id="cfg_headless">
|
|
|
|
|
- <option value="false" selected>有头(推荐,方便干预)</option>
|
|
|
|
|
- <option value="true">无头</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="grid">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>邮件助手 URL</label>
|
|
|
|
|
- <input id="cfg_mail_helper_url" placeholder="http://ali.ss5.xyz:17373" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>邮箱域名</label>
|
|
|
|
|
- <input id="cfg_mail_domain" placeholder="edu.a4sky.com" />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="grid-3">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>邮箱轮询间隔(秒)</label>
|
|
|
|
|
- <input id="cfg_mail_poll_interval_sec" type="number" min="1" value="4" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>邮箱轮询次数</label>
|
|
|
|
|
- <input id="cfg_mail_poll_max_attempts" type="number" min="5" value="60" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>使用 1 个月免费 promo</label>
|
|
|
|
|
- <select id="cfg_use_promo">
|
|
|
|
|
- <option value="true" selected>是</option>
|
|
|
|
|
- <option value="false">否</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="grid">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>PayPal 短信手机号 (E164)</label>
|
|
|
|
|
- <input id="cfg_phone_e164" placeholder="+15822201173" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>接码 API URL</label>
|
|
|
|
|
- <input id="cfg_sms_api_url" placeholder="http://a.62-us.com/api/get_sms?key=..." />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="grid">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>CPA 地址</label>
|
|
|
|
|
- <input id="cfg_cpa_url" placeholder="http://your-cpa-host:port" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>CPA 管理密钥</label>
|
|
|
|
|
- <input id="cfg_cpa_management_key" placeholder="管理 token" />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <h2 style="margin-top:18px">外网 API 接入</h2>
|
|
|
|
|
- <div class="grid-3">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>API 监听 host</label>
|
|
|
|
|
- <input id="cfg_api_host" placeholder="127.0.0.1 或 0.0.0.0" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>API 端口</label>
|
|
|
|
|
- <input id="cfg_api_port" type="number" min="1" max="65535" placeholder="7791" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>API Token(外网必填)</label>
|
|
|
|
|
- <input id="cfg_api_token" placeholder="留空 = 不校验" />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>CORS Allow-Origin</label>
|
|
|
|
|
- <input id="cfg_api_cors_origin" placeholder="* 或 https://your-frontend.com" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <p class="muted" style="margin-top:6px">
|
|
|
|
|
- 在线 API 文档:<a href="/docs" target="_blank" rel="noopener">/docs</a> ·
|
|
|
|
|
- OpenAPI 规范:<a href="/openapi.json" target="_blank" rel="noopener">/openapi.json</a>
|
|
|
|
|
- </p>
|
|
|
|
|
-
|
|
|
|
|
- <div class="grid">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>全局代理(ChatGPT 注册 / 默认浏览器;留空则直连)</label>
|
|
|
|
|
- <input id="cfg_proxy_url" placeholder="http://user:pass@host:port (留空 = ChatGPT 注册直连)" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>PayPal 独立代理(仅 PayPal 阶段;留空则继承全局)</label>
|
|
|
|
|
- <input id="cfg_paypal_only_proxy" placeholder="http://user:pass@host:port" />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="grid">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>长链生成方式</label>
|
|
|
|
|
- <select id="cfg_long_link_mode">
|
|
|
|
|
- <option value="payurl">payurl.ark2.cn 中转(默认)</option>
|
|
|
|
|
- <option value="local">本地直连 ChatGPT API</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label>长链生成代理(仅本地模式)</label>
|
|
|
|
|
- <input id="cfg_long_link_proxy" placeholder="http://user:pass@host:port(本地模式必填)" />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="row">
|
|
|
|
|
- <button id="save">保存配置</button>
|
|
|
|
|
- <button id="go">开始全自动</button>
|
|
|
|
|
- <button id="stop" class="secondary" disabled>停止</button>
|
|
|
|
|
- <span id="state" class="chip gray">空闲</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h2>当前阶段</h2>
|
|
|
|
|
- <div id="stageBox" class="stage-box">空闲</div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h2>账号进度</h2>
|
|
|
|
|
- <table id="accTable">
|
|
|
|
|
- <thead><tr><th>#</th><th>邮箱</th><th>阶段</th><th>planType</th><th>CPA 文件</th><th>错误</th></tr></thead>
|
|
|
|
|
- <tbody></tbody>
|
|
|
|
|
- </table>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h2>已注册账号库</h2>
|
|
|
|
|
- <div class="row" style="margin-top:0">
|
|
|
|
|
- <button id="refreshAccounts" class="secondary">刷新</button>
|
|
|
|
|
- <select id="accFilter" style="max-width:200px">
|
|
|
|
|
- <option value="">全部状态</option>
|
|
|
|
|
- <option value="registered">已注册</option>
|
|
|
|
|
- <option value="paid">已付款</option>
|
|
|
|
|
- <option value="plus">已 Plus</option>
|
|
|
|
|
- <option value="cpa_uploaded">已上传 CPA</option>
|
|
|
|
|
- <option value="cpa_skipped">CPA 跳过</option>
|
|
|
|
|
- <option value="failed">失败</option>
|
|
|
|
|
- <option value="trial">试用 / 待付款</option>
|
|
|
|
|
- <option value="plus_check_failed">Plus 校验失败</option>
|
|
|
|
|
- <option value="cpa_failed">CPA 上传失败</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- <span class="muted">数据库:<code>data/accounts.db</code></span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <table id="dbTable">
|
|
|
|
|
- <thead><tr>
|
|
|
|
|
- <th class="col-email">邮箱</th>
|
|
|
|
|
- <th class="col-plan">plan</th>
|
|
|
|
|
- <th class="col-stat">状态</th>
|
|
|
|
|
- <th class="col-cpa">CPA 文件</th>
|
|
|
|
|
- <th class="col-time">注册时间</th>
|
|
|
|
|
- <th class="col-time">更新时间</th>
|
|
|
|
|
- <th class="col-err">错误</th>
|
|
|
|
|
- <th class="col-act">动作</th>
|
|
|
|
|
- </tr></thead>
|
|
|
|
|
- <tbody></tbody>
|
|
|
|
|
- </table>
|
|
|
|
|
- <div id="dbPager" class="pager"></div>
|
|
|
|
|
- <details style="margin-top:10px">
|
|
|
|
|
- <summary class="muted">点击查看选中账号详情</summary>
|
|
|
|
|
- <pre id="accDetail" class="log" style="height:240px"></pre>
|
|
|
|
|
- </details>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h2>实时日志</h2>
|
|
|
|
|
- <pre id="log" class="log"></pre>
|
|
|
|
|
- </div>
|
|
|
|
|
-</div>
|
|
|
|
|
-
|
|
|
|
|
-<script>
|
|
|
|
|
-const $ = id => document.getElementById(id);
|
|
|
|
|
-
|
|
|
|
|
-const FIELDS = [
|
|
|
|
|
- ['cfg_account_count','account_count','int'],
|
|
|
|
|
- ['cfg_headless','headless','bool'],
|
|
|
|
|
- ['cfg_mail_helper_url','mail_helper_url','str'],
|
|
|
|
|
- ['cfg_mail_domain','mail_domain','str'],
|
|
|
|
|
- ['cfg_mail_poll_interval_sec','mail_poll_interval_sec','int'],
|
|
|
|
|
- ['cfg_mail_poll_max_attempts','mail_poll_max_attempts','int'],
|
|
|
|
|
- ['cfg_use_promo','use_promo','bool'],
|
|
|
|
|
- ['cfg_phone_e164','phone_e164','str'],
|
|
|
|
|
- ['cfg_sms_api_url','sms_api_url','str'],
|
|
|
|
|
- ['cfg_cpa_url','cpa_url','str'],
|
|
|
|
|
- ['cfg_cpa_management_key','cpa_management_key','str'],
|
|
|
|
|
- ['cfg_proxy_url','proxy_url','str'],
|
|
|
|
|
- ['cfg_paypal_only_proxy','paypal_only_proxy','str'],
|
|
|
|
|
- ['cfg_long_link_mode','long_link_mode','str'],
|
|
|
|
|
- ['cfg_long_link_proxy','long_link_proxy','str'],
|
|
|
|
|
- ['cfg_api_host','api_host','str'],
|
|
|
|
|
- ['cfg_api_port','api_port','int'],
|
|
|
|
|
- ['cfg_api_token','api_token','str'],
|
|
|
|
|
- ['cfg_api_cors_origin','api_cors_origin','str'],
|
|
|
|
|
-];
|
|
|
|
|
-
|
|
|
|
|
-function fillForm(cfg) {
|
|
|
|
|
- for (const [domId, key, kind] of FIELDS) {
|
|
|
|
|
- const el = $(domId);
|
|
|
|
|
- if (!el || cfg[key] === undefined) continue;
|
|
|
|
|
- if (kind === 'bool') {
|
|
|
|
|
- el.value = cfg[key] ? 'true' : 'false';
|
|
|
|
|
- } else {
|
|
|
|
|
- el.value = cfg[key];
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function readForm() {
|
|
|
|
|
- const out = {};
|
|
|
|
|
- for (const [domId, key, kind] of FIELDS) {
|
|
|
|
|
- const el = $(domId);
|
|
|
|
|
- if (!el) continue;
|
|
|
|
|
- let v = el.value;
|
|
|
|
|
- if (kind === 'int') v = Number(v) || 0;
|
|
|
|
|
- else if (kind === 'bool') v = (v === 'true' || v === true);
|
|
|
|
|
- out[key] = v;
|
|
|
|
|
- }
|
|
|
|
|
- return out;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function loadConfig() {
|
|
|
|
|
- const r = await fetch('/api/config');
|
|
|
|
|
- const data = await r.json();
|
|
|
|
|
- fillForm(data);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function saveConfig() {
|
|
|
|
|
- const body = readForm();
|
|
|
|
|
- const r = await fetch('/api/config', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
|
|
|
|
- if (!r.ok) { alert('保存失败 HTTP ' + r.status); return; }
|
|
|
|
|
- const data = await r.json();
|
|
|
|
|
- fillForm(data);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function setState(text, cls) {
|
|
|
|
|
- const el = $('state');
|
|
|
|
|
- el.textContent = text;
|
|
|
|
|
- el.className = 'chip ' + (cls || 'gray');
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-let evtSource = null;
|
|
|
|
|
-function startLogStream() {
|
|
|
|
|
- if (evtSource) evtSource.close();
|
|
|
|
|
- evtSource = new EventSource('/api/log');
|
|
|
|
|
- evtSource.onmessage = e => {
|
|
|
|
|
- if (!e.data) return;
|
|
|
|
|
- const log = $('log');
|
|
|
|
|
- log.textContent += e.data + '\n';
|
|
|
|
|
- log.scrollTop = log.scrollHeight;
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function renderAccounts(accounts) {
|
|
|
|
|
- const tbody = $('accTable').querySelector('tbody');
|
|
|
|
|
- tbody.innerHTML = '';
|
|
|
|
|
- accounts.forEach((a, idx) => {
|
|
|
|
|
- const tr = document.createElement('tr');
|
|
|
|
|
- tr.innerHTML = `<td>${idx+1}</td><td>${a.email||''}</td><td>${a.stage||''}</td><td>${a.planType||''}</td><td>${a.cpaFile||''}</td><td style="color:#a40000">${a.error||''}</td>`;
|
|
|
|
|
- tbody.appendChild(tr);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function refreshStatus() {
|
|
|
|
|
- try {
|
|
|
|
|
- const r = await fetch('/api/status');
|
|
|
|
|
- const data = await r.json();
|
|
|
|
|
- $('stageBox').textContent = data.stage || '空闲';
|
|
|
|
|
- renderAccounts(data.accounts || []);
|
|
|
|
|
- if (data.running) {
|
|
|
|
|
- setState('执行中', 'green');
|
|
|
|
|
- $('go').disabled = true;
|
|
|
|
|
- $('stop').disabled = false;
|
|
|
|
|
- } else {
|
|
|
|
|
- $('go').disabled = false;
|
|
|
|
|
- $('stop').disabled = true;
|
|
|
|
|
- if (data.state === 'done') setState('完成', 'green');
|
|
|
|
|
- else if (data.state === 'error') setState('异常', 'red');
|
|
|
|
|
- else if (data.state === 'stopped') setState('已停止', 'red');
|
|
|
|
|
- else setState('空闲', 'gray');
|
|
|
|
|
- }
|
|
|
|
|
- } catch (_) {}
|
|
|
|
|
-}
|
|
|
|
|
-setInterval(refreshStatus, 1500);
|
|
|
|
|
-refreshStatus();
|
|
|
|
|
-startLogStream();
|
|
|
|
|
-loadConfig();
|
|
|
|
|
-loadAccounts();
|
|
|
|
|
-
|
|
|
|
|
-function fmtTime(ms) {
|
|
|
|
|
- if (!ms) return '';
|
|
|
|
|
- const d = new Date(Number(ms));
|
|
|
|
|
- if (isNaN(d.getTime())) return '';
|
|
|
|
|
- const pad = n => String(n).padStart(2,'0');
|
|
|
|
|
- // 紧凑成两行:MM-DD\n HH:MM:SS(避免一行被挤断)
|
|
|
|
|
- return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-let _accPage = 1;
|
|
|
|
|
-const _accPageSize = 20;
|
|
|
|
|
-
|
|
|
|
|
-async function loadAccounts(page) {
|
|
|
|
|
- if (page !== undefined) _accPage = page;
|
|
|
|
|
- const status = $('accFilter').value || '';
|
|
|
|
|
- let url = '/api/accounts?page=' + _accPage + '&pageSize=' + _accPageSize;
|
|
|
|
|
- if (status) url += '&status=' + encodeURIComponent(status);
|
|
|
|
|
- try {
|
|
|
|
|
- const r = await fetch(url);
|
|
|
|
|
- const data = await r.json();
|
|
|
|
|
- const total = data.total || 0;
|
|
|
|
|
- const totalPages = Math.max(1, Math.ceil(total / _accPageSize));
|
|
|
|
|
- if (_accPage > totalPages) _accPage = totalPages;
|
|
|
|
|
- const tbody = $('dbTable').querySelector('tbody');
|
|
|
|
|
- tbody.innerHTML = '';
|
|
|
|
|
- (data.accounts || []).forEach(a => {
|
|
|
|
|
- const tr = document.createElement('tr');
|
|
|
|
|
- const detailBtn = `<button class="action-btn detail" data-email="${a.email}" data-action="detail">详情</button>`;
|
|
|
|
|
- const dlBtn = a.cpa_file_name
|
|
|
|
|
- ? `<a class="action-btn download" href="/api/account/${encodeURIComponent(a.email)}/cpa.json" target="_blank" rel="noopener" download>下载 CPA</a>`
|
|
|
|
|
- : '';
|
|
|
|
|
- const failedStatuses = ['plus_check_failed','cpa_failed','failed','cpa_skipped','registered','paid'];
|
|
|
|
|
- const recheckBtn = failedStatuses.includes(a.final_status||'')
|
|
|
|
|
- ? `<button class="action-btn recheck" data-email="${a.email}" data-action="recheck">重新校验</button>`
|
|
|
|
|
- : '';
|
|
|
|
|
- const retryPayBtn = a.can_retry_payment
|
|
|
|
|
- ? `<button class="action-btn retry-pay" data-email="${a.email}" data-action="retry-pay">重新付款</button>`
|
|
|
|
|
- : '';
|
|
|
|
|
- const emailHtml = `<code title="${a.email||''}">${a.email||''}</code>`;
|
|
|
|
|
- const cpaHtml = a.cpa_file_name ? `<span title="${a.cpa_file_name}">${a.cpa_file_name}</span>` : '';
|
|
|
|
|
- const errHtml = a.last_error ? `<span title="${(a.last_error||'').replace(/"/g,'"')}">${a.last_error}</span>` : '';
|
|
|
|
|
- const statusHtml = `${a.final_status||''}${a.trial_eligible ? ' <span class="chip blue">试用</span>' : ''}`;
|
|
|
|
|
- tr.innerHTML = `<td class="col-email">${emailHtml}</td><td class="col-plan">${a.plan_type||''}</td><td class="col-stat">${statusHtml}</td><td class="col-cpa">${cpaHtml}</td><td class="col-time">${fmtTime(a.created_at)}</td><td class="col-time">${fmtTime(a.updated_at)}</td><td class="col-err">${errHtml}</td><td class="col-act">${detailBtn}${recheckBtn}${retryPayBtn}${dlBtn}</td>`;
|
|
|
|
|
- tbody.appendChild(tr);
|
|
|
|
|
- });
|
|
|
|
|
- tbody.querySelectorAll('button[data-action="detail"]').forEach(btn => {
|
|
|
|
|
- btn.addEventListener('click', () => loadAccountDetail(btn.dataset.email));
|
|
|
|
|
- });
|
|
|
|
|
- tbody.querySelectorAll('button[data-action="recheck"]').forEach(btn => {
|
|
|
|
|
- btn.addEventListener('click', () => triggerRecheck(btn));
|
|
|
|
|
- });
|
|
|
|
|
- tbody.querySelectorAll('button[data-action="retry-pay"]').forEach(btn => {
|
|
|
|
|
- btn.addEventListener('click', () => triggerRetryPayment(btn));
|
|
|
|
|
- });
|
|
|
|
|
- renderPager(totalPages, total);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error(e);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function renderPager(totalPages, total) {
|
|
|
|
|
- const box = $('dbPager');
|
|
|
|
|
- box.innerHTML = '';
|
|
|
|
|
- if (totalPages <= 1 && total <= _accPageSize) { box.innerHTML = `<span class="info">共 ${total} 条</span>`; return; }
|
|
|
|
|
- const info = document.createElement('span');
|
|
|
|
|
- info.className = 'info';
|
|
|
|
|
- info.textContent = `共 ${total} 条 · 第 ${_accPage}/${totalPages} 页`;
|
|
|
|
|
- box.appendChild(info);
|
|
|
|
|
- const addBtn = (label, pg, disabled) => {
|
|
|
|
|
- const b = document.createElement('button');
|
|
|
|
|
- b.textContent = label;
|
|
|
|
|
- b.disabled = disabled;
|
|
|
|
|
- if (pg === _accPage) b.classList.add('active');
|
|
|
|
|
- if (!disabled) b.addEventListener('click', () => loadAccounts(pg));
|
|
|
|
|
- box.appendChild(b);
|
|
|
|
|
- };
|
|
|
|
|
- addBtn('«', 1, _accPage <= 1);
|
|
|
|
|
- addBtn('‹', _accPage - 1, _accPage <= 1);
|
|
|
|
|
- let start = Math.max(1, _accPage - 2);
|
|
|
|
|
- let end = Math.min(totalPages, start + 4);
|
|
|
|
|
- if (end - start < 4) start = Math.max(1, end - 4);
|
|
|
|
|
- for (let i = start; i <= end; i++) addBtn(String(i), i, false);
|
|
|
|
|
- addBtn('›', _accPage + 1, _accPage >= totalPages);
|
|
|
|
|
- addBtn('»', totalPages, _accPage >= totalPages);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function loadAccountDetail(email) {
|
|
|
|
|
- try {
|
|
|
|
|
- const r = await fetch('/api/account/' + encodeURIComponent(email));
|
|
|
|
|
- const data = await r.json();
|
|
|
|
|
- $('accDetail').textContent = JSON.stringify(data, null, 2);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- $('accDetail').textContent = String(e);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function triggerRecheck(btn) {
|
|
|
|
|
- const email = btn.dataset.email;
|
|
|
|
|
- if (!email) return;
|
|
|
|
|
- const orig = btn.textContent;
|
|
|
|
|
- btn.disabled = true;
|
|
|
|
|
- btn.textContent = '校验中...';
|
|
|
|
|
- try {
|
|
|
|
|
- const r = await fetch('/api/account/' + encodeURIComponent(email) + '/recheck', {method:'POST'});
|
|
|
|
|
- const data = await r.json();
|
|
|
|
|
- if (data.ok) {
|
|
|
|
|
- const action = data.action || '';
|
|
|
|
|
- const tip = action === 'cpa_uploaded' ? '已上传 CPA'
|
|
|
|
|
- : action === 'plus_no_cpa_config' ? '已 Plus(未配置 CPA)'
|
|
|
|
|
- : '已 Plus';
|
|
|
|
|
- btn.textContent = '✓ ' + tip;
|
|
|
|
|
- } else if (data.action === 'still_not_plus') {
|
|
|
|
|
- btn.textContent = `仍非 plus(${data.planType||'?'})`;
|
|
|
|
|
- } else {
|
|
|
|
|
- btn.textContent = '✗ 失败';
|
|
|
|
|
- console.warn('recheck error', data);
|
|
|
|
|
- }
|
|
|
|
|
- setTimeout(() => loadAccounts(), 1200);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- alert(e.message || String(e));
|
|
|
|
|
- btn.disabled = false;
|
|
|
|
|
- btn.textContent = orig;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function triggerRetryPayment(btn) {
|
|
|
|
|
- const email = btn.dataset.email;
|
|
|
|
|
- if (!email) return;
|
|
|
|
|
- if (!confirm(`确认为 ${email} 重新发起付款流程?`)) return;
|
|
|
|
|
- const orig = btn.textContent;
|
|
|
|
|
- btn.disabled = true;
|
|
|
|
|
- btn.textContent = '获取中...';
|
|
|
|
|
- try {
|
|
|
|
|
- const r = await fetch('/api/account/' + encodeURIComponent(email) + '/retry_payment', {method:'POST'});
|
|
|
|
|
- const data = await r.json();
|
|
|
|
|
- if (data.ok) {
|
|
|
|
|
- btn.textContent = '✓ 已提交 ' + (data.task_id || '');
|
|
|
|
|
- } else {
|
|
|
|
|
- btn.textContent = '✗ ' + (data.error || '失败');
|
|
|
|
|
- }
|
|
|
|
|
- setTimeout(() => loadAccounts(), 1500);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- alert(e.message || String(e));
|
|
|
|
|
- btn.disabled = false;
|
|
|
|
|
- btn.textContent = orig;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-$('refreshAccounts').addEventListener('click', () => loadAccounts());
|
|
|
|
|
-$('accFilter').addEventListener('change', () => loadAccounts(1));
|
|
|
|
|
-
|
|
|
|
|
-// 全自动跑完后自动刷一次账号库
|
|
|
|
|
-const _origRefreshStatus = refreshStatus;
|
|
|
|
|
-let _wasRunning = false;
|
|
|
|
|
-async function refreshStatusWithDb() {
|
|
|
|
|
- await _origRefreshStatus();
|
|
|
|
|
- try {
|
|
|
|
|
- const r = await fetch('/api/status');
|
|
|
|
|
- const s = await r.json();
|
|
|
|
|
- if (_wasRunning && !s.running) loadAccounts();
|
|
|
|
|
- _wasRunning = !!s.running;
|
|
|
|
|
- } catch (_) {}
|
|
|
|
|
-}
|
|
|
|
|
-clearInterval(window.__statusTimer);
|
|
|
|
|
-window.__statusTimer = setInterval(refreshStatusWithDb, 1500);
|
|
|
|
|
-
|
|
|
|
|
-$('save').addEventListener('click', saveConfig);
|
|
|
|
|
-
|
|
|
|
|
-$('go').addEventListener('click', async () => {
|
|
|
|
|
- // 自动先保存一次配置
|
|
|
|
|
- await saveConfig();
|
|
|
|
|
- $('log').textContent = '';
|
|
|
|
|
- $('go').disabled = true;
|
|
|
|
|
- setState('启动中', 'gray');
|
|
|
|
|
- try {
|
|
|
|
|
- const r = await fetch('/api/start', {method:'POST'});
|
|
|
|
|
- const data = await r.json();
|
|
|
|
|
- if (data.error) {
|
|
|
|
|
- alert(data.error);
|
|
|
|
|
- $('go').disabled = false;
|
|
|
|
|
- setState('空闲', 'gray');
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- alert(e.message || String(e));
|
|
|
|
|
- $('go').disabled = false;
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-$('stop').addEventListener('click', async () => {
|
|
|
|
|
- await fetch('/api/stop', {method: 'POST'});
|
|
|
|
|
-});
|
|
|
|
|
-</script>
|
|
|
|
|
-</body>
|
|
|
|
|
-</html>"""
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
def _read_json(handler) -> dict:
|
|
def _read_json(handler) -> dict:
|
|
|
length = int(handler.headers.get("content-length") or "0")
|
|
length = int(handler.headers.get("content-length") or "0")
|
|
|
if length <= 0:
|
|
if length <= 0:
|
|
@@ -674,7 +134,14 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
path = self.path.split("?", 1)[0]
|
|
path = self.path.split("?", 1)[0]
|
|
|
query = self.path.split("?", 1)[1] if "?" in self.path else ""
|
|
query = self.path.split("?", 1)[1] if "?" in self.path else ""
|
|
|
if path in ("/", "/index.html"):
|
|
if path in ("/", "/index.html"):
|
|
|
- self._send(200, INDEX_HTML.encode("utf-8"), "text/html; charset=utf-8")
|
|
|
|
|
|
|
+ self._send_static_file(UI_DIR / "index.html")
|
|
|
|
|
+ return
|
|
|
|
|
+ if path.startswith("/static/"):
|
|
|
|
|
+ name = path[len("/static/"):]
|
|
|
|
|
+ if "/" in name or "\\" in name or not name:
|
|
|
|
|
+ self._send_json(404, {"error": "not found"})
|
|
|
|
|
+ return
|
|
|
|
|
+ self._send_static_file(UI_DIR / name)
|
|
|
return
|
|
return
|
|
|
if path in ("/docs", "/docs/"):
|
|
if path in ("/docs", "/docs/"):
|
|
|
self._send(200, SWAGGER_HTML.encode("utf-8"), "text/html; charset=utf-8")
|
|
self._send(200, SWAGGER_HTML.encode("utf-8"), "text/html; charset=utf-8")
|
|
@@ -792,8 +259,11 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
try:
|
|
try:
|
|
|
body = _read_json(self) or {}
|
|
body = _read_json(self) or {}
|
|
|
mode = (body.get("mode") or "full").strip().lower()
|
|
mode = (body.get("mode") or "full").strip().lower()
|
|
|
- if mode not in ("full", "pay_only"):
|
|
|
|
|
- self._send_json(400, {"error": "mode 必须是 full 或 pay_only"})
|
|
|
|
|
|
|
+ if mode == "full":
|
|
|
|
|
+ self._send_json(410, {"error": "full 全自动注册任务已停用,请使用 SSO 注册入口"})
|
|
|
|
|
+ return
|
|
|
|
|
+ if mode != "pay_only":
|
|
|
|
|
+ self._send_json(400, {"error": "mode 必须是 pay_only(full 已停用)"})
|
|
|
return
|
|
return
|
|
|
params = body.get("params") or {}
|
|
params = body.get("params") or {}
|
|
|
if mode == "pay_only":
|
|
if mode == "pay_only":
|
|
@@ -825,15 +295,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
if path == "/api/start":
|
|
if path == "/api/start":
|
|
|
- try:
|
|
|
|
|
- cfg = AppConfig.load()
|
|
|
|
|
- err = JOB.start(cfg)
|
|
|
|
|
- if err:
|
|
|
|
|
- self._send_json(409, {"error": err})
|
|
|
|
|
- else:
|
|
|
|
|
- self._send_json(200, {"ok": True})
|
|
|
|
|
- except Exception as exc:
|
|
|
|
|
- self._send_json(500, {"error": str(exc)})
|
|
|
|
|
|
|
+ self._send_json(410, {"error": "ChatGPT Plus 全自动注册已停用,请使用 SSO 注册入口"})
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
if path.startswith("/api/account/") and path.endswith("/recheck"):
|
|
if path.startswith("/api/account/") and path.endswith("/recheck"):
|
|
@@ -875,6 +337,30 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
self._send_json(500, {"error": str(exc)})
|
|
self._send_json(500, {"error": str(exc)})
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ if path == "/api/start-sso":
|
|
|
|
|
+ try:
|
|
|
|
|
+ body = _read_json(self)
|
|
|
|
|
+ account_count = int(body.get("account_count") or 0)
|
|
|
|
|
+ if account_count < 1:
|
|
|
|
|
+ self._send_json(400, {"error": "account_count 必须 >= 1"})
|
|
|
|
|
+ return
|
|
|
|
|
+ cfg = AppConfig.load()
|
|
|
|
|
+ err = JOB.start_sso(
|
|
|
|
|
+ account_count=account_count,
|
|
|
|
|
+ sso_mail_domain=str(body.get("sso_mail_domain") or cfg.sso_mail_domain or "aef.claudeai.life"),
|
|
|
|
|
+ cpa_url=str(body.get("cpa_url") or cfg.cpa_url or ""),
|
|
|
|
|
+ cpa_management_key=str(body.get("cpa_management_key") or cfg.cpa_management_key or ""),
|
|
|
|
|
+ headless=bool(body.get("headless")) if "headless" in body else cfg.headless,
|
|
|
|
|
+ proxy_url=str(body.get("proxy_url") or ""),
|
|
|
|
|
+ )
|
|
|
|
|
+ if err:
|
|
|
|
|
+ self._send_json(409, {"error": err})
|
|
|
|
|
+ else:
|
|
|
|
|
+ self._send_json(200, {"ok": True})
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ self._send_json(500, {"error": str(exc)})
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
if path == "/api/stop":
|
|
if path == "/api/stop":
|
|
|
JOB.stop()
|
|
JOB.stop()
|
|
|
self._send_json(200, {"ok": True})
|
|
self._send_json(200, {"ok": True})
|
|
@@ -910,6 +396,19 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
def _send_json(self, status: int, payload: dict):
|
|
def _send_json(self, status: int, payload: dict):
|
|
|
self._send(status, json.dumps(payload, ensure_ascii=False).encode("utf-8"), "application/json; charset=utf-8")
|
|
self._send(status, json.dumps(payload, ensure_ascii=False).encode("utf-8"), "application/json; charset=utf-8")
|
|
|
|
|
|
|
|
|
|
+ def _send_static_file(self, file_path: Path):
|
|
|
|
|
+ try:
|
|
|
|
|
+ resolved = file_path.resolve()
|
|
|
|
|
+ if UI_DIR.resolve() not in resolved.parents and resolved != (UI_DIR / "index.html").resolve():
|
|
|
|
|
+ self._send_json(404, {"error": "not found"})
|
|
|
|
|
+ return
|
|
|
|
|
+ content = resolved.read_bytes()
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ self._send_json(404, {"error": "not found"})
|
|
|
|
|
+ return
|
|
|
|
|
+ content_type = STATIC_TYPES.get(resolved.suffix.lower(), "application/octet-stream")
|
|
|
|
|
+ self._send(200, content, content_type)
|
|
|
|
|
+
|
|
|
def _send(self, status: int, content: bytes, content_type: str):
|
|
def _send(self, status: int, content: bytes, content_type: str):
|
|
|
self.send_response(status)
|
|
self.send_response(status)
|
|
|
self.send_header("Content-Type", content_type)
|
|
self.send_header("Content-Type", content_type)
|
|
@@ -1013,13 +512,13 @@ def _build_openapi_spec() -> dict:
|
|
|
return {
|
|
return {
|
|
|
"openapi": "3.1.0",
|
|
"openapi": "3.1.0",
|
|
|
"info": {
|
|
"info": {
|
|
|
- "title": "ChatGPT Plus 自动化 API",
|
|
|
|
|
|
|
+ "title": "Auto PayPal API",
|
|
|
"version": "1.0.0",
|
|
"version": "1.0.0",
|
|
|
"description": (
|
|
"description": (
|
|
|
- "ChatGPT Plus 全自动注册 + PayPal 付款 + CPA 上传。\n\n"
|
|
|
|
|
- "**两种模式**:\n"
|
|
|
|
|
- "- `full` — 全自动注册新 ChatGPT 账号,注册→付款→上传 CPA\n"
|
|
|
|
|
- "- `pay_only` — 传入已有 session JSON,跳过注册直接付款→上传 CPA\n\n"
|
|
|
|
|
|
|
+ "SSO 注册、账号库查询与已有 session 付款任务 API。\n\n"
|
|
|
|
|
+ "**可用任务模式**:\n"
|
|
|
|
|
+ "- `pay_only` — 传入已有 session JSON,跳过注册直接付款→上传 CPA\n"
|
|
|
|
|
+ "- `full` 全自动注册模式已停用;请使用 Web UI 的 SSO 注册入口。\n\n"
|
|
|
"**调用流程**:\n"
|
|
"**调用流程**:\n"
|
|
|
"1. POST `/api/tasks` 创建任务,立即拿到 `task_id`\n"
|
|
"1. POST `/api/tasks` 创建任务,立即拿到 `task_id`\n"
|
|
|
"2. 轮询 GET `/api/tasks/{task_id}` 看 `status` 和 `stage`\n"
|
|
"2. 轮询 GET `/api/tasks/{task_id}` 看 `status` 和 `stage`\n"
|
|
@@ -1064,7 +563,7 @@ def _build_openapi_spec() -> dict:
|
|
|
"type": "object",
|
|
"type": "object",
|
|
|
"required": ["mode"],
|
|
"required": ["mode"],
|
|
|
"properties": {
|
|
"properties": {
|
|
|
- "mode": {"type": "string", "enum": ["full", "pay_only"]},
|
|
|
|
|
|
|
+ "mode": {"type": "string", "enum": ["pay_only"]},
|
|
|
"max_attempts": {"type": "integer", "default": 3, "minimum": 1, "maximum": 10},
|
|
"max_attempts": {"type": "integer", "default": 3, "minimum": 1, "maximum": 10},
|
|
|
"params": {
|
|
"params": {
|
|
|
"type": "object",
|
|
"type": "object",
|
|
@@ -1119,21 +618,13 @@ def _build_openapi_spec() -> dict:
|
|
|
"post": {
|
|
"post": {
|
|
|
"tags": ["Tasks"],
|
|
"tags": ["Tasks"],
|
|
|
"summary": "创建任务",
|
|
"summary": "创建任务",
|
|
|
- "description": "创建一个 full 或 pay_only 任务,立即返回 task_id,任务异步执行。",
|
|
|
|
|
|
|
+ "description": "创建一个 pay_only 任务,立即返回 task_id,任务异步执行。full 全自动注册已停用。",
|
|
|
"requestBody": {
|
|
"requestBody": {
|
|
|
"required": True,
|
|
"required": True,
|
|
|
"content": {
|
|
"content": {
|
|
|
"application/json": {
|
|
"application/json": {
|
|
|
"schema": {"$ref": "#/components/schemas/CreateTaskRequest"},
|
|
"schema": {"$ref": "#/components/schemas/CreateTaskRequest"},
|
|
|
"examples": {
|
|
"examples": {
|
|
|
- "full": {
|
|
|
|
|
- "summary": "全自动注册",
|
|
|
|
|
- "value": {
|
|
|
|
|
- "mode": "full",
|
|
|
|
|
- "max_attempts": 3,
|
|
|
|
|
- "params": {},
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
"pay_only": {
|
|
"pay_only": {
|
|
|
"summary": "传入 session 直接付款",
|
|
"summary": "传入 session 直接付款",
|
|
|
"value": {
|
|
"value": {
|
|
@@ -1169,6 +660,7 @@ def _build_openapi_spec() -> dict:
|
|
|
},
|
|
},
|
|
|
"400": {"description": "参数错误", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
|
"400": {"description": "参数错误", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
|
|
"401": {"description": "Bearer token 缺失或无效"},
|
|
"401": {"description": "Bearer token 缺失或无效"},
|
|
|
|
|
+ "410": {"description": "full 全自动注册已停用"},
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
"get": {
|
|
"get": {
|
|
@@ -1312,7 +804,7 @@ def main():
|
|
|
host = (cfg.api_host or HOST).strip() or HOST
|
|
host = (cfg.api_host or HOST).strip() or HOST
|
|
|
port = int(cfg.api_port or PORT)
|
|
port = int(cfg.api_port or PORT)
|
|
|
server = ThreadingHTTPServer((host, port), Handler)
|
|
server = ThreadingHTTPServer((host, port), Handler)
|
|
|
- print(f"ChatGPT Plus Auto Console:")
|
|
|
|
|
|
|
+ print(f"Auto PayPal Console:")
|
|
|
print(f" Web UI: http://{host}:{port}/")
|
|
print(f" Web UI: http://{host}:{port}/")
|
|
|
print(f" Docs: http://{host}:{port}/docs")
|
|
print(f" Docs: http://{host}:{port}/docs")
|
|
|
print(f" OpenAPI: http://{host}:{port}/openapi.json")
|
|
print(f" OpenAPI: http://{host}:{port}/openapi.json")
|