Skip to content

Commit 73308f6

Browse files
committed
Release v1.0.6
1 parent ce0e9ec commit 73308f6

36 files changed

Lines changed: 1168 additions & 178 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 1.0.6 - 2026-07-20
4+
5+
+ [新增] macOS 本地 Captcha Solver 支持 Docker/Xvfb 运行有头 Chromium,浏览器不再弹到桌面,并提供 Compose、缓存持久化和一键启动脚本。
6+
+ [优化] Turnstile 从全局单锁升级为共享动态并发限制器;注册中心可配置注册解题并发、单次解题超时、排队超时和本地尝试次数。
7+
+ [优化] 浏览器上下文和 CloakBrowser 进程在成功、失败与超时后统一释放,降低批量注册时的内存、PID 和残留 Chromium 数量。
8+
+ [修复] Grok Device Code OAuth 复用注册任务的真实代理,并将代理继续透传给登录 Turnstile,修复 OAuth `sign-in` 实际走直连导致的 `600010` 和持续无 token。
9+
+ [变更] Grok 注册成功后立即进入单线程 OAuth 上传队列,不再等待整批注册结束;注册运行时自动预留一个额外 solver 槽位,注册线程与上传互不串行。
10+
+ [优化] OAuth 的 Turnstile、登录、consent 和 token 等瞬时失败自动有限重试;授权队列保持单线程,避免多个 OAuth `sign-in` 同时争抢浏览器资源。
11+
+ [新增] NovaApi 与 CPA 在 OAuth 完成后按启用配置并行即时投递,分别记录成功、失败或跳过状态,并在注册日志中显示明确终态。
12+
+ [文档] 更新 README、macOS/Ubuntu 从零部署、NovaApi/CPA 自动上传和日志排错教程,说明 Docker/Xvfb、即时上传、额外 OAuth 槽位及内存调优方式。
13+
314
## 1.0.5 - 2026-07-19
415

516
+ [修复] OpenAI 注册默认切回 `PlatformRegistrar`,对齐上游最新 authorize/passwordless signup 链路,保留传统密码流程作为显式回退。

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ GPTGrok2API 是一个自托管的 GPT 与 Grok 统一网关,将已接入的订
2323
| iCloud 定时创建 | 按 Apple 账号定时创建邮箱;新接口每小时最多 20 个、旧接口每小时最多 5 个,每账号累计 750 个后自动停止。sidecar 使用 Compose 内部网络,不需要独立账号或宿主机端口。 |
2424
| 邮箱平台标签 | 同一邮箱可分别标记 GPT 和 Grok;GPT 标签为绿色、Grok 标签为蓝色,两个标签同时存在才算已使用,注册领取按目标平台隔离。 |
2525
| 注册中心 | 支持 OpenAI 与 Grok 注册任务,整合临时邮箱、GPTMail、Outlook Token、Microsoft Alias 和 iCloud Privacy Mail;内置 iCloud provider 不需要填写域名、API Base 或 API Key。 |
26-
| 本地 Captcha Solver | 源码内置于 `captcha-solver/`,使用 CloakBrowser/Chromium 处理 Grok Turnstile 实页校验,支持注册代理按请求透传,不需要额外克隆第二个仓库。 |
26+
| 本地 Captcha Solver | 源码内置于 `captcha-solver/`,使用 Docker/Xvfb 中的有头 CloakBrowser/Chromium 处理 Grok Turnstile,支持动态并发、代理透传和浏览器资源回收,不需要额外克隆第二个仓库。 |
2727
| Checkout 提链 | 注册 Checkout 仅保留 UPI 最终支付链接提取;IN Checkout、Provider、Approve 共享同一 sticky 出口,VN Promotion 使用独立代理持续轮换重试。 |
2828
| 代理与稳定出口 | 支持全局代理、账号代理、代理配置、代理组、节点并发限制、故障反馈、备用出口、WARP、Privoxy、FlareSolverr 和 Clearance 刷新。 |
29-
| 外部系统接入 | 支持从 Sub2API、远程 CPA、本地 CPA 和 Access Token 导入账号;OpenAI/Grok 注册后可自动投递 NovaApi CPA;服务器部署提供 `gptgrok2api` Docker 网络别名。 |
29+
| 外部系统接入 | 支持从 Sub2API、远程 CPA、本地 CPA 和 Access Token 导入账号;Grok 注册成功后立即顺序完成 OAuth,并按配置并行投递 NovaApi CPA;服务器部署提供 `gptgrok2api` Docker 网络别名。 |
3030
| 管理控制台 | 提供概览、GPT/Grok 账号管理、Grok Runtime、iCloud 邮箱、注册任务、Checkout 任务、代理管理、日志、实时监控、图片管理、调试中心和系统设置。 |
3131
| 存储与备份 | 账号数据支持 JSON、SQLite、PostgreSQL 和 Git 后端;图片支持本地与 WebDAV;备份支持本地归档和 R2。 |
3232
| 运维与发布 | 支持 Docker Compose、WARP 编排、内置 sidecar、Nginx HTTPS、运行日志、指标趋势、健康检查和 GitHub Releases 更新源。 |
@@ -59,7 +59,7 @@ uv venv --python 3.13 .venv
5959
uv pip install --python .venv/bin/python -r requirements.txt
6060
```
6161

62-
macOS 使用 `deploy/launchd/com.chatgpt2api.captcha-solver.plist.example`Ubuntu/Linux 使用 `deploy/systemd/captcha-solver.service.example`两份模板都默认从 `<项目目录>/captcha-solver` 启动,详细步骤见 macOS/Ubuntu 新手手册。
62+
macOS 需要“有头但不弹窗”时,推荐使用 `deploy/docker-compose.captcha-solver.yml` 在 Docker/Xvfb 中运行;原生 LaunchAgent 会显示 Chromium 窗口。Ubuntu/Linux 使用 `deploy/systemd/captcha-solver.service.example`。详细步骤见 macOS/Ubuntu 新手手册。
6363

6464
## 系统结构
6565

@@ -114,6 +114,7 @@ curl https://pro.muyuai.top/v1/chat/completions \
114114
服务模板:
115115

116116
- macOS:[主程序 LaunchAgent](deploy/launchd/com.chatgpt2api.app.plist.example) / [Captcha Solver LaunchAgent](deploy/launchd/com.chatgpt2api.captcha-solver.plist.example)
117+
- macOS 隐藏有头浏览器:[Captcha Solver Docker/Xvfb Compose](deploy/docker-compose.captcha-solver.yml)
117118
- Ubuntu:[主程序 systemd unit](deploy/systemd/chatgpt2api.service.example) / [Captcha Solver systemd unit](deploy/systemd/captcha-solver.service.example)
118119

119120
## Docker 部署

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.5
1+
1.0.6

captcha-solver/.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.venv/
2+
__pycache__/
3+
*.pyc
4+
*.pyo
5+
*.log
6+
.DS_Store

captcha-solver/Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM mcr.microsoft.com/playwright/python:v1.61.0-noble
2+
3+
ENV PYTHONUNBUFFERED=1 \
4+
PIP_NO_CACHE_DIR=1 \
5+
TURNSTILE_HEADLESS=0 \
6+
SOLVER_ALLOW_PRIVATE=1 \
7+
TURNSTILE_CONCURRENCY=2 \
8+
SOLVER_LOOPBACK_PROXY_HOST=host.docker.internal
9+
10+
WORKDIR /app
11+
12+
COPY requirements.txt ./requirements.txt
13+
RUN python -m pip install --no-cache-dir -r requirements.txt
14+
15+
COPY . .
16+
RUN chmod +x ./run-docker.sh
17+
18+
EXPOSE 8877
19+
20+
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=4 \
21+
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8877/health', timeout=3)"
22+
23+
CMD ["./run-docker.sh"]

captcha-solver/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,32 @@ browser.
121121
| ----------------------- | ------- | ------------------------------------------- |
122122
| `PORT` | `8877` | Listen port |
123123
| `TURNSTILE_HEADLESS` | `1` | `0` = run Turnstile headed |
124+
| `TURNSTILE_HIDE_WINDOW` | unset | `1` = request an off-screen headed window (window managers may clamp it back on-screen) |
125+
| `SOLVER_LOOPBACK_PROXY_HOST` | unset | Rewrite `127.0.0.1`/`localhost` in per-request proxies, normally `host.docker.internal` inside Docker Desktop |
126+
127+
For a 16 GB Mac, start Grok registration at 3 worker threads and local
128+
concurrency 3. The tuned defaults use a 45-second active solve budget, a
129+
separate 60-second queue budget, and up to 3 browser attempts. Queue waiting no
130+
longer consumes the active browser solve deadline.
131+
132+
### macOS: headed mode without desktop windows
133+
134+
macOS clamps native Chromium windows back onto a visible display. For a truly
135+
background headed browser, run the solver in Docker Desktop with Linux Xvfb:
136+
137+
```bash
138+
docker compose -f deploy/docker-compose.captcha-solver.yml build
139+
docker compose -f deploy/docker-compose.captcha-solver.yml up -d
140+
curl http://127.0.0.1:8877/health
141+
```
142+
143+
The browser remains headed inside the container's virtual display. The Compose
144+
service persists CloakBrowser downloads in a named volume and rewrites a
145+
per-request proxy such as `http://127.0.0.1:40080` to
146+
`http://host.docker.internal:40080`, so it can still reach a proxy on the Mac.
124147
| `TURNSTILE_PROXY` | unset | Proxy URL for Turnstile browser |
125148
| `TURNSTILE_GEOIP` | unset | `1` = align browser timezone/locale/WebGL to the proxy exit IP (shared by Turnstile + cloudflare + awswaf) |
149+
| `TURNSTILE_CONCURRENCY` | `2` | Default simultaneous Turnstile browser attempts; Grok requests override it dynamically, including one reserved OAuth slot during immediate upload |
126150
| `RECAPTCHA_HEADLESS` | `0` | `1` = run reCAPTCHA headless |
127151
| `RECAPTCHA_PROXY` | unset | Proxy URL for reCAPTCHA browser |
128152
| `RECAPTCHA_GEOIP` | unset | `1` = same geo alignment for the reCAPTCHA browser |

captcha-solver/common/browser.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import logging
1010
import os
1111
import re
12+
from urllib.parse import urlsplit, urlunsplit
1213

1314
log = logging.getLogger(__name__)
1415

@@ -24,6 +25,25 @@
2425
)
2526

2627

28+
def _rewrite_loopback_proxy(proxy: str) -> str:
29+
"""Map a host-loopback proxy to Docker Desktop's host gateway when requested."""
30+
replacement = str(os.getenv("SOLVER_LOOPBACK_PROXY_HOST") or "").strip()
31+
if not proxy or not replacement:
32+
return proxy
33+
34+
try:
35+
parts = urlsplit(proxy)
36+
except ValueError:
37+
return proxy
38+
if parts.hostname not in {"127.0.0.1", "localhost", "::1"}:
39+
return proxy
40+
41+
userinfo = parts.netloc.rsplit("@", 1)[0] + "@" if "@" in parts.netloc else ""
42+
host = f"[{replacement}]" if ":" in replacement and not replacement.startswith("[") else replacement
43+
port = f":{parts.port}" if parts.port is not None else ""
44+
return urlunsplit((parts.scheme, f"{userinfo}{host}{port}", parts.path, parts.query, parts.fragment))
45+
46+
2747
async def resolve_selector(page, selector: str, timeout: int = 10000):
2848
"""Resolve a selector string to a Playwright locator.
2949
@@ -86,12 +106,15 @@ def browser_kwargs(prefix: str, proxy: str = None) -> dict:
86106
the interactive-checkbox solvers (recaptcha/hcaptcha) default headless=0.
87107
"""
88108
default_headless = "1" if prefix == "TURNSTILE" else "0"
89-
kw = {"humanize": True,
90-
"headless": os.getenv(f"{prefix}_HEADLESS", default_headless) != "0"}
109+
headless = os.getenv(f"{prefix}_HEADLESS", default_headless) != "0"
110+
kw = {"humanize": True, "headless": headless}
111+
if not headless and os.getenv(f"{prefix}_HIDE_WINDOW") == "1":
112+
kw["args"] = ["--window-position=-32000,-32000", "--window-size=1280,900"]
113+
kw["_suppress_maximize"] = True
91114
configured_proxy = str(proxy or os.getenv(f"{prefix}_PROXY") or "").strip()
92115
if configured_proxy:
93-
kw["proxy"] = configured_proxy
94-
if os.getenv(f"{prefix}_GEOIP") == "1":
116+
kw["proxy"] = _rewrite_loopback_proxy(configured_proxy)
117+
if configured_proxy and os.getenv(f"{prefix}_GEOIP") == "1":
95118
kw["geoip"] = True
96119
return kw
97120

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import os
2+
import unittest
3+
from unittest.mock import patch
4+
5+
from common.browser import browser_kwargs
6+
7+
8+
class BrowserKwargsTest(unittest.TestCase):
9+
def test_hidden_headed_mode_uses_offscreen_window(self):
10+
with patch.dict(
11+
os.environ,
12+
{"TURNSTILE_HEADLESS": "0", "TURNSTILE_HIDE_WINDOW": "1"},
13+
clear=False,
14+
):
15+
kwargs = browser_kwargs("TURNSTILE")
16+
17+
self.assertFalse(kwargs["headless"])
18+
self.assertIn("--window-position=-32000,-32000", kwargs["args"])
19+
self.assertIn("--window-size=1280,900", kwargs["args"])
20+
self.assertTrue(kwargs["_suppress_maximize"])
21+
22+
def test_headless_mode_does_not_add_window_args(self):
23+
with patch.dict(
24+
os.environ,
25+
{"TURNSTILE_HEADLESS": "1", "TURNSTILE_HIDE_WINDOW": "1"},
26+
clear=False,
27+
):
28+
kwargs = browser_kwargs("TURNSTILE")
29+
30+
self.assertTrue(kwargs["headless"])
31+
self.assertNotIn("args", kwargs)
32+
self.assertNotIn("_suppress_maximize", kwargs)
33+
34+
def test_docker_rewrites_loopback_proxy_to_host_gateway(self):
35+
with (
36+
patch.dict(
37+
os.environ,
38+
{"SOLVER_LOOPBACK_PROXY_HOST": "host.docker.internal"},
39+
clear=False,
40+
),
41+
):
42+
kwargs = browser_kwargs("TURNSTILE", "http://user:pass@127.0.0.1:40080")
43+
44+
self.assertEqual(
45+
kwargs["proxy"],
46+
"http://user:pass@host.docker.internal:40080",
47+
)
48+
49+
def test_docker_keeps_non_loopback_proxy_unchanged(self):
50+
with patch.dict(
51+
os.environ,
52+
{"SOLVER_LOOPBACK_PROXY_HOST": "host.docker.internal"},
53+
clear=False,
54+
):
55+
kwargs = browser_kwargs("TURNSTILE", "http://10.0.0.8:8080")
56+
57+
self.assertEqual(kwargs["proxy"], "http://10.0.0.8:8080")
58+
59+
60+
if __name__ == "__main__":
61+
unittest.main()

captcha-solver/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
# (run.sh sources it); this file documents the runtime deps, it is not auto-installed.
33
fastapi
44
uvicorn
5+
websockets
56
pydantic
6-
cloakbrowser
7+
cloakbrowser[geoip]
78
pillow
89
# Note: whisper/curl are NOT required — the audio-challenge path was removed
910
# (it was dead code and reliably IP-blocked). reCAPTCHA/hCaptcha image challenges

captcha-solver/run-docker.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
python -c 'import cloakbrowser; cloakbrowser.ensure_binary()'
5+
Xvfb :99 -screen 0 1920x1080x24 -ac -nolisten tcp &
6+
export DISPLAY=:99
7+
8+
exec python -m uvicorn server:app --host 0.0.0.0 --port 8877

0 commit comments

Comments
 (0)