Skip to content

Commit a543efc

Browse files
committed
Release v1.2.0
1 parent f8f8e63 commit a543efc

50 files changed

Lines changed: 3836 additions & 335 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

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

3+
## 1.2.0 - 2026-07-25
4+
5+
+ [新增] Grok 注册支持在同一有头浏览器会话内完成中文资料填写、邮箱确认、Turnstile 与账号创建,并增加页面状态诊断、截图和确认邮件兜底。
6+
+ [新增] Grok OAuth 账号轮询改为显式开关,关闭后立即停止历史无额度复检、自动重新授权和未授权回填;开启后立即调度并持久化状态。
7+
+ [新增] Grok OAuth 日志按授权阶段自动清理,账号页支持批量重新授权,并完整显示相关邮箱账号与处理状态。
8+
+ [修复] Device OAuth 补齐 Cookie 会话复用、consent 状态识别和权限待生效复检,明确区分拒绝授权、真实无额度、限流与上游未返回额度。
9+
+ [修复] Grok 账号统计和列表使用上游 billing 返回的真实请求及 Token 额度,未知额度不再错误显示为满额或无额度。
10+
+ [优化] ZooProxy 注册任务自动生成独立 SID,并在单个任务的注册、验证、Turnstile 和 OAuth 全链路保持同一出口;Playwright 兼容 `socks5h` 代理配置。
11+
+ [工具] 增加 Grok 4.5 账号批量探测与无额度账号导出清理脚本。
12+
313
## 1.1.0 - 2026-07-23
414

515
+ [修复] Grok PKCE 能识别 `accounts.x.ai` consent Server Action 返回的 `Access denied`,明确标记为账号未获 Build/API OAuth 授权资格且不可重试,不再误报为 OAuth redirect loop。

‎VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.0
1+
1.2.0

‎captcha-solver/common/browser.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ def _rewrite_loopback_proxy(proxy: str) -> str:
4444
return urlunsplit((parts.scheme, f"{userinfo}{host}{port}", parts.path, parts.query, parts.fragment))
4545

4646

47+
def _playwright_proxy_url(proxy: str) -> str:
48+
candidate = _rewrite_loopback_proxy(proxy)
49+
if candidate.lower().startswith("socks5h://"):
50+
return "socks5://" + candidate[len("socks5h://") :]
51+
return candidate
52+
53+
4754
async def resolve_selector(page, selector: str, timeout: int = 10000):
4855
"""Resolve a selector string to a Playwright locator.
4956
@@ -102,8 +109,8 @@ async def run_pre_actions(page, actions: list):
102109
def browser_kwargs(prefix: str, proxy: str = None) -> dict:
103110
"""Browser-trust levers, env-configurable per solver.
104111
105-
prefix is TURNSTILE | RECAPTCHA | HCAPTCHA. Turnstile defaults headless=1;
106-
the interactive-checkbox solvers (recaptcha/hcaptcha) default headless=0.
112+
prefix is TURNSTILE | RECAPTCHA | HCAPTCHA | XAI. Turnstile defaults
113+
headless=1; interactive flows (recaptcha/hcaptcha/xAI) default headless=0.
107114
"""
108115
default_headless = "1" if prefix == "TURNSTILE" else "0"
109116
headless = os.getenv(f"{prefix}_HEADLESS", default_headless) != "0"
@@ -113,7 +120,7 @@ def browser_kwargs(prefix: str, proxy: str = None) -> dict:
113120
kw["_suppress_maximize"] = True
114121
configured_proxy = str(proxy or os.getenv(f"{prefix}_PROXY") or "").strip()
115122
if configured_proxy:
116-
kw["proxy"] = _rewrite_loopback_proxy(configured_proxy)
123+
kw["proxy"] = _playwright_proxy_url(configured_proxy)
117124
if configured_proxy and os.getenv(f"{prefix}_GEOIP") == "1":
118125
kw["geoip"] = True
119126
return kw

‎captcha-solver/common/browser_test.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ def test_docker_keeps_non_loopback_proxy_unchanged(self):
5656

5757
self.assertEqual(kwargs["proxy"], "http://10.0.0.8:8080")
5858

59+
def test_playwright_uses_supported_socks5_scheme_for_socks5h_proxy(self):
60+
kwargs = browser_kwargs(
61+
"XAI",
62+
"socks5h://proxy-user:proxy-password@proxy.example.test:5000",
63+
)
64+
65+
self.assertEqual(
66+
kwargs["proxy"],
67+
"socks5://proxy-user:proxy-password@proxy.example.test:5000",
68+
)
69+
5970

6071
if __name__ == "__main__":
6172
unittest.main()

‎captcha-solver/server.py‎

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ class SolveRequest(BaseModel):
210210
concurrency: Optional[int] = Field(
211211
None, ge=0, le=64, description="Turnstile-only maximum simultaneous browser attempts. "
212212
"Use 0 for unlimited; otherwise overrides TURNSTILE_CONCURRENCY.")
213+
cookies: Optional[list[dict[str, Any]]] = Field(
214+
None, description="Browser cookies to restore before real-page navigation.")
213215

214216
# reCAPTCHA-only
215217
version: Optional[str] = Field(None, description="reCAPTCHA only: v2 | v3 | invisible (default v2).")
@@ -232,6 +234,41 @@ class SolveRequest(BaseModel):
232234
render_flow: Optional[str] = Field(None, description="perimeterx: named site trigger that makes the gate render when it doesn't show on plain load (default 'outlook_signup'). Throwaway navigation only — NOT account creation. Pass null with a `url` for deployments whose gate renders on goto(). Harvests the _px3 clearance cookie (bound to _pxvid+IP+UA; replay under the same proxy+UA within TTL).")
233235

234236

237+
class XaiSessionStartRequest(BaseModel):
238+
signup_url: str = "https://accounts.x.ai/sign-up?redirect=grok-com"
239+
proxy: Optional[str] = None
240+
timeout_s: int = Field(120, ge=15, le=180)
241+
242+
243+
class XaiSessionEmailRequest(BaseModel):
244+
session_id: str
245+
email: str
246+
247+
248+
class XaiSessionVerifyRequest(XaiSessionEmailRequest):
249+
code: str
250+
251+
252+
class XaiSessionSignupRequest(XaiSessionVerifyRequest):
253+
password: str
254+
given_name: str
255+
family_name: str
256+
turnstile_token: str
257+
action_id: str
258+
next_router_state_tree: str
259+
260+
261+
class XaiSessionAuthorizeRequest(BaseModel):
262+
session_id: str
263+
verification_url: str
264+
user_code: str
265+
timeout_s: int = Field(120, ge=15, le=600)
266+
267+
268+
class XaiSessionCloseRequest(BaseModel):
269+
session_id: str
270+
271+
235272
# Named request examples → Swagger UI renders these as a dropdown picker on /solve.
236273
_SOLVE_EXAMPLES = {
237274
"turnstile": {
@@ -344,6 +381,102 @@ async def health():
344381
return {"status": "ok", "supported_types": SUPPORTED}
345382

346383

384+
def _xai_http_error(error: Exception) -> HTTPException:
385+
from xai_browser.flow import XaiBrowserFlowError
386+
387+
if isinstance(error, XaiBrowserFlowError):
388+
status = 409 if error.stage in {"browser_session", "cookie_setter", "approve"} else 400
389+
if error.retryable:
390+
status = 503
391+
return HTTPException(status, detail=str(error))
392+
return HTTPException(500, detail=f"xAI browser flow failed: {type(error).__name__}")
393+
394+
395+
@app.post("/xai/session/start", dependencies=[Depends(_bearer)], tags=["solve"])
396+
async def xai_session_start(req: XaiSessionStartRequest):
397+
from xai_browser.flow import start_session
398+
399+
_assert_public_url(req.signup_url, "signup_url")
400+
try:
401+
return await start_session(
402+
signup_url=req.signup_url,
403+
proxy=req.proxy or "",
404+
timeout_s=req.timeout_s,
405+
)
406+
except Exception as error:
407+
raise _xai_http_error(error) from error
408+
409+
410+
@app.post("/xai/session/send-email", dependencies=[Depends(_bearer)], tags=["solve"])
411+
async def xai_session_send_email(req: XaiSessionEmailRequest):
412+
from xai_browser.flow import send_email
413+
414+
try:
415+
return await send_email(req.session_id, req.email)
416+
except Exception as error:
417+
raise _xai_http_error(error) from error
418+
419+
420+
@app.post("/xai/session/verify-email", dependencies=[Depends(_bearer)], tags=["solve"])
421+
async def xai_session_verify_email(req: XaiSessionVerifyRequest):
422+
from xai_browser.flow import verify_email
423+
424+
try:
425+
return await verify_email(req.session_id, req.email, req.code)
426+
except Exception as error:
427+
raise _xai_http_error(error) from error
428+
429+
430+
@app.post("/xai/session/signup", dependencies=[Depends(_bearer)], tags=["solve"])
431+
async def xai_session_signup(req: XaiSessionSignupRequest):
432+
from xai_browser.flow import signup
433+
434+
try:
435+
return await signup(
436+
req.session_id,
437+
email=req.email,
438+
password=req.password,
439+
code=req.code,
440+
given_name=req.given_name,
441+
family_name=req.family_name,
442+
turnstile_token=req.turnstile_token,
443+
action_id=req.action_id,
444+
next_router_state_tree=req.next_router_state_tree,
445+
)
446+
except Exception as error:
447+
raise _xai_http_error(error) from error
448+
449+
450+
@app.post("/xai/session/authorize-device", dependencies=[Depends(_bearer)], tags=["solve"])
451+
async def xai_session_authorize_device(req: XaiSessionAuthorizeRequest):
452+
from xai_browser.flow import authorize_device
453+
454+
_assert_public_url(req.verification_url, "verification_url")
455+
try:
456+
return await authorize_device(
457+
req.session_id,
458+
verification_url=req.verification_url,
459+
user_code=req.user_code,
460+
timeout_s=req.timeout_s,
461+
)
462+
except Exception as error:
463+
raise _xai_http_error(error) from error
464+
465+
466+
@app.post("/xai/session/close", dependencies=[Depends(_bearer)], tags=["solve"])
467+
async def xai_session_close(req: XaiSessionCloseRequest):
468+
from xai_browser.flow import close_session
469+
470+
return {"ok": True, "closed": await close_session(req.session_id)}
471+
472+
473+
@app.on_event("shutdown")
474+
async def close_xai_browser_sessions():
475+
from xai_browser.flow import close_all_sessions
476+
477+
await close_all_sessions()
478+
479+
347480
def _extract(req: SolveRequest):
348481
"""Unpack pre_actions + post_fetch for realpage endpoints."""
349482
actions = [a.model_dump() for a in req.pre_actions] if req.pre_actions else None
@@ -371,7 +504,8 @@ async def _dispatch(req: SolveRequest) -> dict:
371504
actions, fetches = _extract(req)
372505
r = await solve_turnstile_realpage(
373506
req.url, req.sitekey, req.timeout_s, actions, fetches, req.proxy,
374-
req.action, req.cdata, req.concurrency, req.queue_timeout_s)
507+
req.action, req.cdata, req.concurrency, req.queue_timeout_s,
508+
req.cookies)
375509
else:
376510
r = await solve_turnstile(
377511
req.sitekey, req.url, req.action, req.cdata, req.proxy,

0 commit comments

Comments
 (0)