Add Luban SMS provider for OpenAI phone verification
This commit is contained in:
@@ -0,0 +1,444 @@
|
|||||||
|
---
|
||||||
|
name: codex-oauth-plus-onboarding
|
||||||
|
description: "Low-token, stable workflow for BOSS's Codex/OpenAI OAuth onboarding: ClawEmail signup, optional 5sim SMS phone verification, and callback import into CPA/Codex2API/SUB2API."
|
||||||
|
version: 2.0.0
|
||||||
|
author: Hermes Agent
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [codex, oauth, clawemail, 5sim, cpa, codex2api, sub2api, browser-automation]
|
||||||
|
related_skills: [clawemail-operations, browser-extension-storage-audit]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Codex OAuth + CPA/Codex2API Onboarding — low-token runbook
|
||||||
|
|
||||||
|
Use when BOSS asks to register/login an OpenAI/Codex OAuth account, verify it via ClawEmail and possibly 5sim SMS, then import the OAuth callback into **CPA / CLI Proxy API**, Codex2API, or SUB2API.
|
||||||
|
|
||||||
|
Safety: BOSS-owned infra/accounts only. Stop for CAPTCHA/security challenge/payment confirmation unless BOSS explicitly approves the next step. Never reveal API keys, passwords, proxy URLs, full phone numbers, OAuth `code=`, SMS/email codes, admin keys, or tokens; report `[REDACTED]`.
|
||||||
|
|
||||||
|
## 0. Required companion skill
|
||||||
|
|
||||||
|
Before ClawEmail work, load:
|
||||||
|
|
||||||
|
```text
|
||||||
|
clawemail-operations
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Low-token operating mode
|
||||||
|
|
||||||
|
Prefer script-driven runs that emit compact JSONL. Only send screenshots or long logs when a gate fails.
|
||||||
|
|
||||||
|
### Tool/reporting rules
|
||||||
|
|
||||||
|
- Read secrets only from `~/.hermes/env/codex_oauth_onboarding.env`; do not paste them into chat.
|
||||||
|
- **Do not blindly `source` this env file in shell**: values such as `CHROME_BIN=/Applications/Google Chrome.app/...` may contain spaces and break `source` if unquoted. For probes/runners, parse `KEY=VALUE` safely in Python or require quoted values before shell sourcing.
|
||||||
|
- Reports should contain: phase, account email, country slug, activation id, high-level status, HTTP status, and redacted error class.
|
||||||
|
- Avoid dumping HTML, callback URLs, headers, full 5sim responses, or full phone numbers.
|
||||||
|
- Save detailed evidence to `references/` or local logs; final response stays concise.
|
||||||
|
|
||||||
|
## 2. Environment constants for BOSS
|
||||||
|
|
||||||
|
```text
|
||||||
|
env: ~/.hermes/env/codex_oauth_onboarding.env
|
||||||
|
workspace: /Users/chick/.Hermes/workspace
|
||||||
|
OAuth Chrome CDP default: 9223
|
||||||
|
ClawEmail Dashboard CDP default: 9224
|
||||||
|
OAuth profile root: /Users/chick/.Hermes/workspace/browser-profiles/codex-oauth
|
||||||
|
Dashboard profile: /Users/chick/.Hermes/workspace/browser-profiles/clawemail-dashboard-persistent
|
||||||
|
Preferred browser proxy when unspecified: socks5://192.168.2.8:1085
|
||||||
|
Chrome: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
Use visible Google Chrome for OpenAI/Auth pages. Do not use BOSS's personal browser profile.
|
||||||
|
|
||||||
|
## 3. Hard gates — do not skip
|
||||||
|
|
||||||
|
1. **Target gate**: exactly one target mode: `cpa`, `codex2api`, or `sub2api`. No default if env is empty.
|
||||||
|
2. **Password gate**: `CUSTOM_PASSWORD` must be set unless BOSS approves generating one.
|
||||||
|
3. **Proxy gate**: verify proxy TCP and OpenAI reachability before browser flow.
|
||||||
|
4. **Mailbox gate**: new ClawEmail sub-mailbox must have external receive enabled before OpenAI signup.
|
||||||
|
5. **OpenAI identity gate before phone**: only buy 5sim if active page is a usable `https://auth.openai.com/add-phone` for a non-deactivated account.
|
||||||
|
6. **Country gate before phone buy/submit**: OpenAI select value and visible dial code must match target country before buying and again before clicking Continue.
|
||||||
|
7. **SMS-only gate**: if OpenAI resend channel probes as WhatsApp, do not click it; cancel/rotate under SMS-only policy.
|
||||||
|
8. **Callback state gate**: callback `state` must match generated OAuth state when known.
|
||||||
|
9. **Payment gate**: any Plus/GoPay paid action needs explicit BOSS confirmation.
|
||||||
|
|
||||||
|
## 4. Recommended decision tree
|
||||||
|
|
||||||
|
```text
|
||||||
|
Need OAuth import?
|
||||||
|
├─ Target CPA available? → prefer CPA (most reliable after 2026-05-10 success)
|
||||||
|
├─ Codex2API requested? → try Codex2API; if token exchange returns unsupported_country_region_territory, switch/fix backend proxy or use CPA
|
||||||
|
└─ SUB2API requested? → use original extension/content-script path
|
||||||
|
|
||||||
|
Need new OpenAI account?
|
||||||
|
├─ Create ClawEmail sub-mailbox with 8 lowercase letters
|
||||||
|
├─ Enable external receive via Dashboard API
|
||||||
|
├─ Submit email/password, poll ClawEmail code
|
||||||
|
└─ If phone page appears → 5sim SMS flow
|
||||||
|
|
||||||
|
Already phone-verified account?
|
||||||
|
└─ Generate fresh target OAuth URL, login/consent, submit callback to target
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. ClawEmail sub-mailbox gate
|
||||||
|
|
||||||
|
BOSS preference: per-run ClawEmail prefix is **exactly 8 lowercase letters** with no `codex` prefix, e.g. `chickliu.ktqcxzux@claw.163.com`.
|
||||||
|
|
||||||
|
After creation, enable external receive. Observed Dashboard API base:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://claw.163.com/mailserv-claw-dashboard/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use relative `/api/v1/...` if it returns public ClawEmail HTML. Working sequence: `GET <base>/workspaces` -> `GET <base>/mailboxes?workspaceId=<workspaceId>` -> find the target sub-mailbox id -> `POST <base>/mailboxes/comm-settings?id=<mailboxId>` with the payload above. If controlling the persistent Dashboard Chrome on CDP `9224` fails with `Handshake status 403 Forbidden ... remote-allow-origins`, use `websocket.create_connection(ws_url, suppress_origin=True)` or restart Chrome with `--remote-allow-origins=*`.
|
||||||
|
|
||||||
|
Required final values:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"commLevel":2,"extReceiveType":1,"extSendType":0}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use Dashboard persistent Chrome/profile for master login, not the disposable OAuth profile. If Dashboard login needs a master QQ mailbox code and the agent cannot retrieve it, ask BOSS. If automating through CDP, Chrome may reject WebSocket Origin unless launched with `--remote-allow-origins=*`; Python `websocket-client` can connect with `suppress_origin=True`. If relative `/api/v1/workspaces` returns raw/empty data, inspect Dashboard resource URLs and try the full `https://claw.163.com/mailserv-claw-dashboard/api/v1/...` base before diagnosing mailbox absence; see `clawemail-operations` reference `references/clawemail-dashboard-cdp-origin-and-api-base-pitfalls-2026-05-10.md`.
|
||||||
|
|
||||||
|
Key references:
|
||||||
|
|
||||||
|
- `references/clawemail-external-receive-gate-openai-2026-05-10.md`
|
||||||
|
- `references/clawemail-random-suffix-policy-2026-05.md`
|
||||||
|
|
||||||
|
## 6. OpenAI email/profile flow
|
||||||
|
|
||||||
|
Minimal flow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fresh OAuth URL / signup page
|
||||||
|
submit ClawEmail sub-mailbox using real CDP input events, not only `input.value=`
|
||||||
|
submit CUSTOM_PASSWORD using real CDP input events
|
||||||
|
poll ClawEmail inbox/spam for OpenAI code using a mail-cli config/profile that points to the exact sub-mailbox
|
||||||
|
submit code using real CDP input events
|
||||||
|
fill profile/about-you if shown
|
||||||
|
continue to phone or OAuth consent
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenAI Auth can route a fresh sub-mailbox first to `log-in/password`; if the password attempt returns `Incorrect email address or password`, click visible `注册` / `Sign up` and continue on `create-account`. Navigation after code/password submit can close the inspected CDP target; reattach and inspect before retrying.
|
||||||
|
|
||||||
|
`mail-cli` default profile may still point to `chickliu@claw.163.com`; it will not show mail delivered to `chickliu.<suffix>@claw.163.com`. For sub-mailbox code polling, create a temp config/profile for that UID before `mail list`/`read body`; see `clawemail-operations` reference `references/submailbox-profile-polling-openai-code-2026-05-10.md`.
|
||||||
|
|
||||||
|
Expected fields observed in 2026-05-11 run:
|
||||||
|
|
||||||
|
```text
|
||||||
|
input[name=name] type=text required
|
||||||
|
input[name=age] type=number required
|
||||||
|
input[name=birthday] type=hidden
|
||||||
|
```
|
||||||
|
|
||||||
|
Set name and a valid numeric age explicitly with the native input value setter and dispatch `input`/`change`; optionally set hidden birthday consistently. Do not fill every input with the same generic text.
|
||||||
|
|
||||||
|
If OpenAI returns `account_deactivated`, stop that account immediately and do not buy 5sim numbers.
|
||||||
|
|
||||||
|
If login recovery hits `Incorrect email address or password`, `invalid_state`, stale email verification, or tab ambiguity, stop before buying phone numbers; recover account/page first. For existing accounts with password failure, the login-page `使用一次性验证码登录` path may send an email code, but `mail-cli` output is not guaranteed newest-first: parse/sort the `date` field and submit the newest OpenAI login mail. Do not use passwordless signup (`使用一次性验证码注册`) for an existing account; it can return `passwordless_signup_disabled`. If a fresh email-code submission returns `account_deactivated`, stop the account immediately even if phone verification previously succeeded.
|
||||||
|
|
||||||
|
## 7. 5sim SMS phone flow
|
||||||
|
|
||||||
|
If login recovery hits `Incorrect email address or password`, `invalid_state`, stale email verification, or tab ambiguity, stop before buying phone numbers; recover account/page first. For existing accounts with password failure, the login-page `使用一次性验证码登录` path may send an email code, but `mail-cli` output is not guaranteed newest-first: parse/sort the `date` field and submit the newest OpenAI login mail. Do not use passwordless signup (`使用一次性验证码注册`) for an existing account; it can return `passwordless_signup_disabled`. If a fresh email-code submission returns `account_deactivated`, stop the account immediately even if phone verification previously succeeded.
|
||||||
|
|
||||||
|
## 7. 5sim SMS phone flow
|
||||||
|
|
||||||
|
Use SMS activation provider selected by `PHONE_SMS_PROVIDER`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
5sim (default): PHONE_SMS_PROVIDER=5sim
|
||||||
|
Luban SMS: PHONE_SMS_PROVIDER=luban
|
||||||
|
```
|
||||||
|
|
||||||
|
### Luban SMS provider
|
||||||
|
|
||||||
|
Docs: `https://lubansms.com/api_docs/`. All endpoints are GET under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://lubansms.com/v2/api
|
||||||
|
```
|
||||||
|
|
||||||
|
Env:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LUBAN_SMS_API_KEY=[REDACTED]
|
||||||
|
LUBAN_SMS_API_BASE=https://lubansms.com/v2/api
|
||||||
|
LUBAN_SMS_SERVICE_QUERY=OpenAI
|
||||||
|
LUBAN_SMS_LANGUAGE=en
|
||||||
|
```
|
||||||
|
|
||||||
|
Important API quirk: requests without a browser-like `User-Agent` may return HTTP 403. Use a Chrome UA.
|
||||||
|
|
||||||
|
Core endpoints:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BALANCE: /getBalance?apikey=...
|
||||||
|
SERVICES: /List?apikey=...&country=<countryName>&service=OpenAI&language=en&page=1
|
||||||
|
BUY: /getNumber?apikey=...&service_id=<service_id>
|
||||||
|
CHECK: /getSms?apikey=...&request_id=<request_id>
|
||||||
|
CANCEL: /setStatus?apikey=...&request_id=<request_id>&status=reject
|
||||||
|
HISTORY: /smsHistory?apikey=...&language=en&page=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed OpenAI service lookup returns entries such as `OpenAI / ChatGpt` and `OpenAI`; cheapest current match can be resolved with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/Users/chick/homebrew/opt/python@3.11/bin/python3.11 \
|
||||||
|
/Users/chick/.hermes/skills/software-development/codex-oauth-plus-onboarding/scripts/luban_sms.py \
|
||||||
|
resolve --service OpenAI --language en --pages 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified 2026-05-11 with BOSS key: balance API OK, balance `2.000`; service list OK. Do not print the key. The helper script is at `scripts/luban_sms.py`.
|
||||||
|
|
||||||
|
If using the long-running runner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PHONE_SMS_PROVIDER=luban START_USED_NUMBERS=<prior> MAX_NEW_NUMBERS=<cap> CDP_PORT=<port> \
|
||||||
|
/Users/chick/homebrew/opt/python@3.11/bin/python3.11 \
|
||||||
|
/Users/chick/.hermes/skills/software-development/codex-oauth-plus-onboarding/scripts/phone_verify_until_success.py
|
||||||
|
```
|
||||||
|
|
||||||
|
5sim legacy API:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PHONE_SMS_PROVIDER=5sim
|
||||||
|
FIVE_SIM_OPERATOR=any
|
||||||
|
FIVE_SIM_PRODUCT=openai
|
||||||
|
FIVE_SIM_SERVICE=openai
|
||||||
|
BUY: /v1/user/buy/activation/{country}/any/openai
|
||||||
|
CHECK: /v1/user/check/{activation_id}
|
||||||
|
FINISH: /v1/user/finish/{activation_id}
|
||||||
|
CANCEL: /v1/user/cancel/{activation_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
Country precedence for configured runs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
FIVE_SIM_COUNTRY_ORDER > FIVE_SIM_COUNTRY_ID > vietnam fallback only if both empty
|
||||||
|
```
|
||||||
|
|
||||||
|
For free-country exploration, ignore stale configured order if BOSS explicitly says unrestricted. Count only real activations with ids toward the budget. `no free phones`, pre-buy country-gate errors, CDP errors, or OAuth recovery failures are not attempts.
|
||||||
|
|
||||||
|
Known mappings:
|
||||||
|
|
||||||
|
```text
|
||||||
|
vietnam -> VN -> 越南 (+84) -> 84
|
||||||
|
argentina -> AR -> 阿根廷 (+54) -> 54
|
||||||
|
netherlands -> NL -> 荷兰 (+31) -> 31
|
||||||
|
indonesia -> ID -> 印度尼西亚 (+62) -> 62
|
||||||
|
italy -> IT -> 意大利 (+39) -> 39
|
||||||
|
poland -> PL -> 波兰 (+48) -> 48
|
||||||
|
spain -> ES -> 西班牙 (+34) -> 34
|
||||||
|
england -> GB -> 英国 (+44) -> 44
|
||||||
|
chile -> CL -> 智利 (+56) -> 56
|
||||||
|
portugal -> PT -> 葡萄牙 (+351) -> 351
|
||||||
|
germany -> DE -> 德国 (+49) -> 49
|
||||||
|
romania -> RO -> 罗马尼亚 (+40) -> 40
|
||||||
|
```
|
||||||
|
|
||||||
|
Phone handling:
|
||||||
|
|
||||||
|
```text
|
||||||
|
OpenAI immediate reject (`无法向此电话号码发送验证码`) or invalid-phone (`电话号码无效`) → cancel activation, rotate
|
||||||
|
No immediate reject → poll 5sim
|
||||||
|
Before resend → probe channel text/value/aria/title
|
||||||
|
├─ WhatsApp → cancel/rotate (SMS-only)
|
||||||
|
└─ SMS/unknown → click resend, poll again
|
||||||
|
SMS received → prefer a 6-digit code candidate for OpenAI phone verification; submit code, verify page leaves phone-verification, then finish activation
|
||||||
|
Interrupted runner → inspect/cancel active RECEIVED activations immediately
|
||||||
|
```
|
||||||
|
|
||||||
|
If a broad SMS regex finds 4-8 digit candidates, prefer `(?<!\d)(\d{6})(?!\d)` first for OpenAI phone verification. A 2026-05-11 run mistakenly selected a 4-digit candidate from the 5sim SMS blob while the page required 6 characters; the activation was already `FINISHED`, but re-checking the finished activation and extracting a 6-digit candidate allowed successful submission. Do not call 5sim `finish` until the browser leaves `phone-verification` unless you have separately verified the SMS code was accepted.
|
||||||
|
|
||||||
|
When replacing a rejected/invalid number on the same OpenAI add-phone page, clear the visible `input[type=tel]` using real keyboard select-all/backspace before inserting the next number; JS-only `.value=''` can leave the hidden `phoneNumber`/React state stale. Vietnam-only test on 2026-05-10 had 7 immediate rejects/invalids despite correct `VN` / `越南 (+84)` gate; see `references/openai-add-phone-vietnam-only-rejects-2026-05-10.md`. Later the same verified account was relaunched with the same Chrome user-data-dir but proxy changed from `1085` to `1083` (`--proxy-server=socks5://192.168.2.8:1083`, new CDP port `9226`); the page stayed on `add-phone` but country reset to `US`, so rerun the country gate before buying. 1083 + Vietnam still produced 5 immediate rejects/invalids; see `references/openai-vietnam-phone-rejects-proxy1085-1083-2026-05-10.md`.
|
||||||
|
|
||||||
|
A later unrestricted continuation succeeded on the 335th actual activation (Indonesia, 5sim `FINISHED`); see `references/openai-phone-335-success-cpa-oauth-login-session-pitfalls-2026-05-11.md`. That run exposed post-phone pitfalls: fill `about-you` fields explicitly (`name`, `age`, hidden `birthday`), avoid Chrome `/json/new?<oauth_url>` for CPA OAuth because it can produce OpenAI `unknown_error`, and use same-tab `Page.navigate` instead. If phone is already verified but CPA OAuth still fails with `Workspaces not found in client auth session`, `unknown_error`, `Incorrect email address or password`, `invalid_state`, or `passwordless_signup_disabled`, treat it as an Auth/session/credential problem, not an SMS/proxy problem; do not keep buying phone numbers for the same account.
|
||||||
|
|
||||||
|
Multi-country / free-country runner stability:
|
||||||
|
|
||||||
|
- Count only real 5sim activations with ids toward requested totals. `no free phones`, country-gate failures, page recovery failures, and CDP errors do not count.
|
||||||
|
- Log compact JSONL phases: `activation_bought`, `phone_submitted`, `activation_cancel_rejected`, `sms_timeout_cancel`, `sms_received`, and final summary. This makes it possible to top up to an exact activation count after page interruptions.
|
||||||
|
- If OpenAI reaches `phone-verification` but page text says the code was sent by **WhatsApp**, treat it as SMS-only failure: poll 5sim briefly if already purchased, cancel on zero SMS, then explicitly navigate back to `https://auth.openai.com/add-phone` before continuing; otherwise later country gates fail because the page remains on `phone-verification`.
|
||||||
|
- For OpenAI SMS/email verification code extraction, prefer strict 6-digit extraction (`(?<!\d)(\d{6})(?!\d)`) over broad 4–8 digit matching; broad matching can capture unrelated numbers and leave the page on “code incorrect”.
|
||||||
|
- If OpenAI shows `糟糕,出错了! Bad request` after many attempts/cancellations, click retry/reload or navigate directly to `https://auth.openai.com/add-phone`, then verify phone-country controls before buying another number.?!\d)`) over broad 4–8 digit matching; broad matching can capture unrelated numbers and leave the page on “code incorrect”.
|
||||||
|
- If OpenAI shows `糟糕,出错了! Bad request` after many attempts/cancellations, click retry/reload or navigate directly to `https://auth.openai.com/add-phone`, then verify phone-country controls before buying another number.
|
||||||
|
- OpenAI phone/SMS codes are normally 6 digits. Do not extract a generic 4–8 digit token from serialized 5sim objects before checking page requirements; prefer `(?<!\d)(\d{6})(?!\d)` from the SMS message text, and only `finish` the 5sim activation after the page has left `phone-verification`.
|
||||||
|
- If a country repeatedly enters WhatsApp-only verification (e.g. Germany in the 2026-05-11 run), skip/reorder it for top-up attempts unless BOSS explicitly wants to keep testing that country.
|
||||||
|
|
||||||
|
See `references/free-country-openai-phone-20-activation-failure-2026-05-11.md` for the session details: 1083 free-country 20 actual activations, WhatsApp-only Germany/Argentina cases, Bad Request recovery, top-up accounting, and cancellation verification.
|
||||||
|
|
||||||
|
When BOSS asks to keep running “until it works” and report how many numbers were used, use the bundled long-running script instead of manual 20-number chunks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
START_USED_NUMBERS=<prior-real-activations> MAX_NEW_NUMBERS=300 CDP_PORT=<port> \
|
||||||
|
/Users/chick/homebrew/opt/python@3.11/bin/python3.11 \
|
||||||
|
/Users/chick/.hermes/skills/software-development/codex-oauth-plus-onboarding/scripts/phone_verify_until_success.py \
|
||||||
|
| tee /tmp/phone_verify_until_success.log
|
||||||
|
```
|
||||||
|
|
||||||
|
The script emits `new_numbers` and `total_numbers`, persists `/tmp/phone_verify_until_success_state.json`, and keeps `/tmp/current_fivesim_activation_id.txt` updated for interruption cleanup. `MAX_NEW_NUMBERS=0` means unlimited; otherwise keep a safety cap unless BOSS explicitly approves unlimited spend. See `references/until-success-phone-runner-cumulative-accounting-2026-05-11.md`.
|
||||||
|
|
||||||
|
Known result: free-country run succeeded with Vietnam SMS, activation `1006871765`, status `FINISHED`; OpenAI phone verification passed. See `references/openai-phone-free-country-vietnam-sms-success-codex2api-region-block-2026-05-10.md`.
|
||||||
|
|
||||||
|
## 8. CPA / CLI Proxy API — preferred callback target
|
||||||
|
|
||||||
|
CPA succeeded where Codex2API failed. Use this when `CPA_VPS_URL` and `CPA_VPS_PASSWORD` are configured.
|
||||||
|
|
||||||
|
Derive API origin:
|
||||||
|
|
||||||
|
```python
|
||||||
|
origin = new URL(CPA_VPS_URL).origin
|
||||||
|
# e.g. http://192.168.2.62:8317/management.html -> http://192.168.2.62:8317
|
||||||
|
```
|
||||||
|
|
||||||
|
Headers, matching original extension:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Authorization: Bearer [CPA_VPS_PASSWORD]
|
||||||
|
X-Management-Key: [CPA_VPS_PASSWORD]
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate OAuth URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET <origin>/v0/management/codex-auth-url
|
||||||
|
# UI also uses: GET <origin>/v0/management/codex-auth-url?is_webui=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract URL from `url|auth_url|authUrl|data.*`; extract state from payload or URL. In Chrome 147/CDP, opening this URL via `/json/new?...` can route to OpenAI `unknown_error`; prefer navigating the current intended auth tab with `Page.navigate` when retrying fresh CPA URLs.
|
||||||
|
|
||||||
|
Submit callback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST <origin>/v0/management/oauth-callback
|
||||||
|
{"provider":"codex","redirect_url":"http://localhost:1455/auth/callback?code=[REDACTED]&state=[REDACTED]"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET <origin>/v0/management/get-auth-status?state=<state> -> {"status":"ok"}
|
||||||
|
GET <origin>/v0/management/auth-files -> codex-<email>-free.json active
|
||||||
|
```
|
||||||
|
|
||||||
|
OAuth recovery pitfalls after phone success:
|
||||||
|
|
||||||
|
- Do not keep buying phone numbers once OpenAI has left `phone-verification`; report phone success separately from OAuth/CPA import status.
|
||||||
|
- If Codex consent shows `Workspaces not found in client auth session`, the account/session lacks a usable ChatGPT workspace. Establish a valid ChatGPT web session/workspace first, then generate a **new** CPA OAuth URL; repeatedly clicking retry on the same consent error does not fix it.
|
||||||
|
- If one-time-code login is needed after password login fails, request it from a fresh auth page/state. A stale password page may make `使用一次性验证码登录` return `invalid_state`; polling ClawEmail may still find a code, but submitting it on the stale error page will not recover.
|
||||||
|
- If many fresh CPA URLs start returning OpenAI `/error` `unknown_error` despite normal URL parameters, close auth tabs/restart the automation Chrome and consider pausing/restarting CPA before generating another URL.
|
||||||
|
|
||||||
|
Do **not** use root `/codex-auth-url` or `/api/auth/url`; they can 404 or return unrelated `amp upstream proxy not available`.
|
||||||
|
|
||||||
|
References:
|
||||||
|
|
||||||
|
- `references/cpa-original-extension-api-notes-2026-05-10.md`
|
||||||
|
- `references/cpa-panel-codex-oauth-success-2026-05-10.md`
|
||||||
|
|
||||||
|
## 9. Codex2API target
|
||||||
|
|
||||||
|
Generate URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST <origin>/api/admin/oauth/generate-auth-url
|
||||||
|
Header: X-Admin-Key: [CODEX2API_ADMIN_KEY]
|
||||||
|
Body: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected response contains:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"auth_url":"...","session_id":"..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exchange callback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST <origin>/api/admin/oauth/exchange-code
|
||||||
|
Header: X-Admin-Key: [CODEX2API_ADMIN_KEY]
|
||||||
|
{"session_id":"...","code":"[REDACTED]","state":"..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
If callback/token exchange returns `unsupported_country_region_territory`, browser-side OAuth is not the issue. The backend token exchange path needs a supported outbound proxy or use CPA.
|
||||||
|
|
||||||
|
## 10. SUB2API target
|
||||||
|
|
||||||
|
Use original extension/content-script route for SUB2API panel login, group, proxy, priority, and callback exchange. Do not invent direct API endpoints unless re-confirmed from source/UI.
|
||||||
|
|
||||||
|
## 11. Browser/CDP hygiene
|
||||||
|
|
||||||
|
Before fresh OAuth recovery:
|
||||||
|
|
||||||
|
```text
|
||||||
|
close stale auth.openai.com / phone-verification / localhost callback tabs
|
||||||
|
keep only the current intended target tab
|
||||||
|
prefer same-tab Page.navigate for CPA OAuth URLs; avoid Chrome /json/new?<oauth_url> when OpenAI starts returning unknown_error
|
||||||
|
```
|
||||||
|
|
||||||
|
Use visible Chrome with isolated profile. Chrome stable may ignore `--load-extension`; verify extension card/sidepanel if using the extension. Manual CDP flow is acceptable when BOSS only wants registration/OAuth tested; still follow all gates.
|
||||||
|
|
||||||
|
When connecting to Chrome CDP with `websocket-client`, Chrome may reject the WebSocket handshake with `403 Forbidden` unless the client sends an allowed Origin or Chrome was launched with `--remote-allow-origins=*`. Prefer launching automation Chrome with `--remote-allow-origins=*`; for an already-running CDP instance, pass `origin='http://127.0.0.1:<port>'` or a matching allowed origin to `websocket.create_connection(...)` before assuming CDP is down.
|
||||||
|
|
||||||
|
## 12. Plus / GoPay
|
||||||
|
|
||||||
|
Default ordinary account test:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PLUS_MODE_ENABLED=false
|
||||||
|
```
|
||||||
|
|
||||||
|
If Plus is requested:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PLUS_PAYMENT_METHOD=gopay
|
||||||
|
GOPAY_OTP_SOURCE=manual
|
||||||
|
GOPAY_OTP empty by default
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop immediately before paid confirmation and ask BOSS.
|
||||||
|
|
||||||
|
## 13. Verification checklist before final report
|
||||||
|
|
||||||
|
- [ ] New mailbox external receive enabled and recorded.
|
||||||
|
- [ ] OpenAI email verification completed or classified.
|
||||||
|
- [ ] If phone appeared: all active 5sim orders are FINISHED or CANCELED.
|
||||||
|
- [ ] If SMS succeeded: OpenAI left `phone-verification` page.
|
||||||
|
- [ ] OAuth callback submitted to chosen target.
|
||||||
|
- [ ] Target has active account/session/auth file.
|
||||||
|
- [ ] Final report redacts secrets, callback codes, SMS/email codes, full phone numbers, API keys, passwords, proxy strings.
|
||||||
|
- [ ] If new learning occurred, update this skill or a reference.
|
||||||
|
|
||||||
|
## 14. Evidence and history references
|
||||||
|
|
||||||
|
Full pre-rewrite skill was archived at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
references/SKILL-full-before-low-token-rewrite-2026-05-10.md
|
||||||
|
```
|
||||||
|
|
||||||
|
High-value references:
|
||||||
|
|
||||||
|
```text
|
||||||
|
references/env-parsing-and-cdp-origin-pitfalls-2026-05-10.md
|
||||||
|
references/cpa-original-extension-api-notes-2026-05-10.md
|
||||||
|
references/cpa-panel-codex-oauth-success-2026-05-10.md
|
||||||
|
references/openai-phone-free-country-vietnam-sms-success-codex2api-region-block-2026-05-10.md
|
||||||
|
references/openai-add-phone-strict-country-gate-2026-05-10.md
|
||||||
|
references/openai-add-phone-free-country-runner-policy-2026-05-10.md
|
||||||
|
references/openai-login-recovery-gate-before-phone-runs-2026-05-10.md
|
||||||
|
references/openai-oauth-tab-cleanup-before-recovery-2026-05-10.md
|
||||||
|
references/openai-phone-country-scheduler-cyclic-chunks-2026-05-10.md
|
||||||
|
references/clawemail-external-receive-gate-openai-2026-05-10.md
|
||||||
|
references/openai-add-phone-chile-no-free-phones-2026-05-10.md
|
||||||
|
references/openai-add-phone-eu4-total20-real-activations-2026-05-10.md
|
||||||
|
references/openai-vietnam-phone-rejects-proxy1085-1083-2026-05-10.md
|
||||||
|
references/free-country-openai-phone-20-activation-failure-2026-05-11.md
|
||||||
|
references/openai-phone-free-country-continuation-1083-2026-05-11.md
|
||||||
|
references/openai-email-code-login-account-deactivated-2026-05-11.md
|
||||||
|
references/official-extension-phone-update-2026-05-10.mdenai-email-code-login-account-deactivated-2026-05-11.md
|
||||||
|
references/official-extension-phone-update-2026-05-10.md
|
||||||
|
```
|
||||||
|
```
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Luban SMS provider integration notes — 2026-05-11
|
||||||
|
|
||||||
|
Source docs: https://lubansms.com/api_docs/
|
||||||
|
|
||||||
|
## API shape
|
||||||
|
|
||||||
|
All endpoints are GET under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://lubansms.com/v2/api
|
||||||
|
```
|
||||||
|
|
||||||
|
Every request uses `apikey=<secret>` query parameter. Do not log it.
|
||||||
|
|
||||||
|
Important quirk found during integration: plain Python urllib requests without a browser-like User-Agent returned HTTP 403. Adding a Chrome-like UA made both `https` and `http` balance requests return HTTP 200.
|
||||||
|
|
||||||
|
## Core endpoints
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /getBalance?apikey=...
|
||||||
|
GET /countries?apikey=...
|
||||||
|
GET /List?apikey=...&country=<countryName>&service=<serviceName>&language=en&page=1
|
||||||
|
GET /getNumber?apikey=...&service_id=<service_id>
|
||||||
|
GET /getSms?apikey=...&request_id=<request_id>
|
||||||
|
GET /setStatus?apikey=...&request_id=<request_id>&status=reject
|
||||||
|
GET /getAgainNmuber?apikey=...&request_id=<request_id>
|
||||||
|
GET /smsHistory?apikey=...&language=en&page=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response mapping
|
||||||
|
|
||||||
|
Buy success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":0,"msg":"","number":"79781901206","request_id":230698}
|
||||||
|
```
|
||||||
|
|
||||||
|
Poll waiting:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":0,"msg":"wait","sms_msg":{"request_id":"244936588","application_id":133,"country_id":1,"number":"79587588703"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Poll success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":0,"msg":"success","sms_code":"380682"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cancel/release:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":0,"msg":"success"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verified with BOSS key
|
||||||
|
|
||||||
|
Do not disclose the key. It was stored in `~/.hermes/env/codex_oauth_onboarding.env` as `LUBAN_SMS_API_KEY`.
|
||||||
|
|
||||||
|
Verification output after adding Chrome UA:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"http":200,"code":0,"ok":true,"balance":"2.000","msg":""}
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenAI service search returned usable entries, including:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"service_id":"537935","country_en":"United Kingdom/England","country_zh":"英国/英格兰","service_name":"OpenAI","provider":"acsim","cost":0.05}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local helper
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/Users/chick/homebrew/opt/python@3.11/bin/python3.11 \
|
||||||
|
/Users/chick/.hermes/skills/software-development/codex-oauth-plus-onboarding/scripts/luban_sms.py balance
|
||||||
|
|
||||||
|
/Users/chick/homebrew/opt/python@3.11/bin/python3.11 \
|
||||||
|
/Users/chick/.hermes/skills/software-development/codex-oauth-plus-onboarding/scripts/luban_sms.py services --service OpenAI --language en --limit 20
|
||||||
|
|
||||||
|
/Users/chick/homebrew/opt/python@3.11/bin/python3.11 \
|
||||||
|
/Users/chick/.hermes/skills/software-development/codex-oauth-plus-onboarding/scripts/luban_sms.py resolve --service OpenAI --language en --pages 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runner integration
|
||||||
|
|
||||||
|
`phone_verify_until_success.py` now supports:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PHONE_SMS_PROVIDER=luban
|
||||||
|
```
|
||||||
|
|
||||||
|
Provider abstraction:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sms_buy(country) -> Luban getNumber(service_id resolved from /List)
|
||||||
|
sms_check(id) -> Luban getSms(request_id)
|
||||||
|
sms_cancel(id) -> Luban setStatus(... status=reject)
|
||||||
|
sms_finish(id) -> no-op success for Luban
|
||||||
|
```
|
||||||
|
|
||||||
|
Country names map from the existing 5sim country slugs to Luban English country names when available.
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Luban SMS API client for Codex/OpenAI phone verification.
|
||||||
|
|
||||||
|
Reads secrets from ~/.hermes/env/codex_oauth_onboarding.env.
|
||||||
|
Never prints API keys, full phone numbers, or SMS codes unless explicitly requested by caller.
|
||||||
|
|
||||||
|
Docs: https://lubansms.com/api_docs/
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
|
ENV_PATH = pathlib.Path.home() / ".hermes/env/codex_oauth_onboarding.env"
|
||||||
|
DEFAULT_BASE = "https://lubansms.com/v2/api"
|
||||||
|
|
||||||
|
|
||||||
|
def load_env(path: pathlib.Path = ENV_PATH) -> Dict[str, str]:
|
||||||
|
env: Dict[str, str] = {}
|
||||||
|
if not path.exists():
|
||||||
|
return env
|
||||||
|
for line in path.read_text().splitlines():
|
||||||
|
s = line.strip()
|
||||||
|
if not s or s.startswith("#") or "=" not in s:
|
||||||
|
continue
|
||||||
|
if s.startswith("export "):
|
||||||
|
s = s[7:].strip()
|
||||||
|
k, v = s.split("=", 1)
|
||||||
|
k = k.strip()
|
||||||
|
v = v.strip()
|
||||||
|
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", k):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if v and v[0] in "'\"":
|
||||||
|
v = shlex.split(v)[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
env[k] = v.strip("'\"")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def redact_phone(phone: Any) -> Optional[str]:
|
||||||
|
if phone is None:
|
||||||
|
return None
|
||||||
|
s = re.sub(r"\D", "", str(phone))
|
||||||
|
if len(s) <= 4:
|
||||||
|
return "[REDACTED]"
|
||||||
|
return "[REDACTED]" + s[-4:]
|
||||||
|
|
||||||
|
|
||||||
|
def strict_code(text: str) -> Optional[str]:
|
||||||
|
m = re.search(r"(?<!\d)(\d{6})(?!\d)", text or "")
|
||||||
|
return m.group(1) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
class LubanSms:
|
||||||
|
def __init__(self, apikey: str, base: str = DEFAULT_BASE, timeout: int = 30):
|
||||||
|
self.apikey = apikey
|
||||||
|
self.base = base.rstrip("/")
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_env(cls) -> "LubanSms":
|
||||||
|
env = load_env()
|
||||||
|
key = env.get("LUBAN_SMS_API_KEY") or env.get("LUBAN_API_KEY")
|
||||||
|
if not key:
|
||||||
|
raise SystemExit("missing LUBAN_SMS_API_KEY in ~/.hermes/env/codex_oauth_onboarding.env")
|
||||||
|
return cls(key, env.get("LUBAN_SMS_API_BASE", DEFAULT_BASE))
|
||||||
|
|
||||||
|
def get(self, endpoint: str, **params: Any) -> Tuple[int, Dict[str, Any], str]:
|
||||||
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
params["apikey"] = self.apikey
|
||||||
|
url = f"{self.base}/{endpoint.lstrip('/')}?{urllib.parse.urlencode(params)}"
|
||||||
|
req = urllib.request.Request(url, headers={
|
||||||
|
"Accept": "application/json",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=self.timeout) as r:
|
||||||
|
raw = r.read().decode(errors="ignore")
|
||||||
|
status = r.status
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
raw = e.read().decode(errors="ignore")
|
||||||
|
status = e.code
|
||||||
|
except Exception as e:
|
||||||
|
return 0, {"code": 0, "msg": f"{type(e).__name__}: {e}"}, ""
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
data = {"raw": raw[:500]}
|
||||||
|
return status, data, raw
|
||||||
|
|
||||||
|
def balance(self) -> Tuple[int, Dict[str, Any], str]:
|
||||||
|
return self.get("getBalance")
|
||||||
|
|
||||||
|
def services(self, country: Optional[str] = None, service: Optional[str] = None, language: str = "en", page: int = 1):
|
||||||
|
return self.get("List", country=country, service=service, language=language, page=page)
|
||||||
|
|
||||||
|
def countries(self):
|
||||||
|
return self.get("countries")
|
||||||
|
|
||||||
|
def buy(self, service_id: str) -> Tuple[int, Dict[str, Any], str]:
|
||||||
|
return self.get("getNumber", service_id=service_id)
|
||||||
|
|
||||||
|
def sms(self, request_id: str) -> Tuple[int, Dict[str, Any], str]:
|
||||||
|
return self.get("getSms", request_id=request_id)
|
||||||
|
|
||||||
|
def reject(self, request_id: str) -> Tuple[int, Dict[str, Any], str]:
|
||||||
|
return self.get("setStatus", request_id=request_id, status="reject")
|
||||||
|
|
||||||
|
def again(self, request_id: str) -> Tuple[int, Dict[str, Any], str]:
|
||||||
|
return self.get("getAgainNmuber", request_id=request_id)
|
||||||
|
|
||||||
|
def history(self, language: str = "en", page: int = 1) -> Tuple[int, Dict[str, Any], str]:
|
||||||
|
return self.get("smsHistory", language=language, page=page)
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_services(data: Dict[str, Any], limit: int = 20) -> List[Dict[str, Any]]:
|
||||||
|
msg = data.get("msg")
|
||||||
|
rows = msg if isinstance(msg, list) else []
|
||||||
|
out: List[Dict[str, Any]] = []
|
||||||
|
for r in rows[:limit]:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
continue
|
||||||
|
out.append({
|
||||||
|
"service_id": str(r.get("service_id", "")),
|
||||||
|
"country_en": r.get("country_name_en") or r.get("country_name") or r.get("country"),
|
||||||
|
"country_zh": r.get("country_name_zh"),
|
||||||
|
"service_name": r.get("service_name"),
|
||||||
|
"provider": r.get("provider"),
|
||||||
|
"cost": r.get("cost"),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_service_id(api: LubanSms, country: Optional[str], service: str, language: str = "en", pages: int = 3) -> Optional[Dict[str, Any]]:
|
||||||
|
# Exact service_name match preferred, then substring match. Keep first cheapest by numeric cost when possible.
|
||||||
|
candidates: List[Dict[str, Any]] = []
|
||||||
|
for page in range(1, pages + 1):
|
||||||
|
st, data, _ = api.services(country=country, service=service, language=language, page=page)
|
||||||
|
if st != 200 or data.get("code") != 0:
|
||||||
|
continue
|
||||||
|
for row in summarize_services(data, limit=500):
|
||||||
|
name = str(row.get("service_name") or "")
|
||||||
|
if service.lower() in name.lower() or name.lower() in service.lower():
|
||||||
|
candidates.append(row)
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
def cost_key(r: Dict[str, Any]) -> float:
|
||||||
|
try: return float(r.get("cost"))
|
||||||
|
except Exception: return 999999.0
|
||||||
|
exact = [r for r in candidates if str(r.get("service_name", "")).lower() == service.lower()]
|
||||||
|
return sorted(exact or candidates, key=cost_key)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_balance(args: argparse.Namespace) -> int:
|
||||||
|
api = LubanSms.from_env()
|
||||||
|
st, data, _ = api.balance()
|
||||||
|
print(json.dumps({"http": st, "code": data.get("code"), "ok": st == 200 and data.get("code") == 0, "balance": data.get("balance"), "msg": data.get("msg")}, ensure_ascii=False))
|
||||||
|
return 0 if st == 200 and data.get("code") == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_services(args: argparse.Namespace) -> int:
|
||||||
|
api = LubanSms.from_env()
|
||||||
|
st, data, _ = api.services(country=args.country, service=args.service, language=args.language, page=args.page)
|
||||||
|
print(json.dumps({"http": st, "code": data.get("code"), "ok": st == 200 and data.get("code") == 0, "services": summarize_services(data, args.limit), "msg_type": type(data.get("msg")).__name__}, ensure_ascii=False))
|
||||||
|
return 0 if st == 200 and data.get("code") == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_resolve(args: argparse.Namespace) -> int:
|
||||||
|
api = LubanSms.from_env()
|
||||||
|
row = resolve_service_id(api, args.country, args.service, args.language, args.pages)
|
||||||
|
print(json.dumps({"ok": bool(row), "selected": row}, ensure_ascii=False))
|
||||||
|
return 0 if row else 2
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_buy(args: argparse.Namespace) -> int:
|
||||||
|
api = LubanSms.from_env()
|
||||||
|
service_id = args.service_id
|
||||||
|
selected = None
|
||||||
|
if not service_id:
|
||||||
|
selected = resolve_service_id(api, args.country, args.service, args.language, args.pages)
|
||||||
|
if not selected:
|
||||||
|
print(json.dumps({"phase": "resolve_service_fail", "country": args.country, "service": args.service}, ensure_ascii=False))
|
||||||
|
return 2
|
||||||
|
service_id = selected["service_id"]
|
||||||
|
st, data, _ = api.buy(service_id)
|
||||||
|
ok = st == 200 and data.get("code") == 0 and data.get("request_id") and data.get("number")
|
||||||
|
print(json.dumps({"http": st, "ok": bool(ok), "code": data.get("code"), "msg": data.get("msg"), "request_id": data.get("request_id"), "phone_redacted": redact_phone(data.get("number")), "selected": selected}, ensure_ascii=False))
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_sms(args: argparse.Namespace) -> int:
|
||||||
|
api = LubanSms.from_env()
|
||||||
|
st, data, _ = api.sms(args.request_id)
|
||||||
|
code = data.get("sms_code") or strict_code(json.dumps(data, ensure_ascii=False))
|
||||||
|
print(json.dumps({"http": st, "ok": st == 200 and data.get("code") == 0, "msg": data.get("msg"), "has_code": bool(code), "code_len": len(str(code)) if code else 0}, ensure_ascii=False))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_reject(args: argparse.Namespace) -> int:
|
||||||
|
api = LubanSms.from_env()
|
||||||
|
st, data, _ = api.reject(args.request_id)
|
||||||
|
print(json.dumps({"http": st, "ok": st == 200 and data.get("code") == 0, "code": data.get("code"), "msg": data.get("msg")}, ensure_ascii=False))
|
||||||
|
return 0 if st == 200 and data.get("code") == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
p = argparse.ArgumentParser(description="Luban SMS API helper")
|
||||||
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
sub.add_parser("balance")
|
||||||
|
s = sub.add_parser("services"); s.add_argument("--country"); s.add_argument("--service", default="OpenAI"); s.add_argument("--language", default="en"); s.add_argument("--page", type=int, default=1); s.add_argument("--limit", type=int, default=30)
|
||||||
|
r = sub.add_parser("resolve"); r.add_argument("--country"); r.add_argument("--service", default="OpenAI"); r.add_argument("--language", default="en"); r.add_argument("--pages", type=int, default=3)
|
||||||
|
b = sub.add_parser("buy"); b.add_argument("--service-id"); b.add_argument("--country"); b.add_argument("--service", default="OpenAI"); b.add_argument("--language", default="en"); b.add_argument("--pages", type=int, default=3)
|
||||||
|
g = sub.add_parser("sms"); g.add_argument("request_id")
|
||||||
|
x = sub.add_parser("reject"); x.add_argument("request_id")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[List[str]] = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
return globals()[f"cmd_{args.cmd}"](args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Continue OpenAI add-phone verification with 5sim until SMS succeeds.
|
||||||
|
|
||||||
|
Designed for codex-oauth-plus-onboarding runs on BOSS infra.
|
||||||
|
Reads secrets from ~/.hermes/env/codex_oauth_onboarding.env and never prints them.
|
||||||
|
Emits compact JSONL and tracks cumulative phone-number usage.
|
||||||
|
|
||||||
|
Environment knobs:
|
||||||
|
CDP_PORT=9226
|
||||||
|
START_USED_NUMBERS=57
|
||||||
|
MAX_NEW_NUMBERS=300 # 0 means unlimited; keep a guard unless BOSS explicitly approves
|
||||||
|
PHONE_POLL_SECONDS=75
|
||||||
|
PER_COUNTRY_ROUND=3
|
||||||
|
"""
|
||||||
|
import json, urllib.request, urllib.error, websocket, itertools, time, pathlib, re, shlex, sys, os
|
||||||
|
|
||||||
|
CDP_PORT=os.environ.get('CDP_PORT','9226')
|
||||||
|
CDP=f'http://127.0.0.1:{CDP_PORT}'
|
||||||
|
POLL_SECONDS=int(os.environ.get('PHONE_POLL_SECONDS','75'))
|
||||||
|
START_USED=int(os.environ.get('START_USED_NUMBERS','0'))
|
||||||
|
MAX_NEW_NUMBERS=int(os.environ.get('MAX_NEW_NUMBERS','300'))
|
||||||
|
PER_COUNTRY_ROUND=int(os.environ.get('PER_COUNTRY_ROUND','3'))
|
||||||
|
COUNTRIES=[
|
||||||
|
('argentina','AR','阿根廷','54'), ('netherlands','NL','荷兰','31'), ('germany','DE','德国','49'),
|
||||||
|
('indonesia','ID','印度尼西亚','62'), ('vietnam','VN','越南','84'), ('italy','IT','意大利','39'),
|
||||||
|
('poland','PL','波兰','48'), ('spain','ES','西班牙','34'), ('england','GB','英国','44'),
|
||||||
|
('france','FR','法国','33'), ('malaysia','MY','马来西亚','60'), ('thailand','TH','泰国','66'),
|
||||||
|
('philippines','PH','菲律宾','63'), ('colombia','CO','哥伦比亚','57'), ('mexico','MX','墨西哥','52'),
|
||||||
|
('brazil','BR','巴西','55'), ('peru','PE','秘鲁','51'), ('turkey','TR','土耳其','90'),
|
||||||
|
('kazakhstan','KZ','哈萨克斯坦','7'), ('chile','CL','智利','56'), ('portugal','PT','葡萄牙','351'),
|
||||||
|
('romania','RO','罗马尼亚','40'),
|
||||||
|
]
|
||||||
|
FIVESIM_TO_LUBAN_COUNTRY={
|
||||||
|
'argentina':'Argentina','netherlands':'Netherlands','germany':'Germany','indonesia':'Indonesia','vietnam':'Vietnam',
|
||||||
|
'italy':'Italy','poland':'Poland','spain':'Spain','england':'United Kingdom/England','france':'France',
|
||||||
|
'malaysia':'Malaysia','thailand':'Thailand','philippines':'Philippines','colombia':'Colombia','mexico':'Mexico',
|
||||||
|
'brazil':'Brazil','peru':'Peru','turkey':'Turkey','kazakhstan':'Kazakhstan','chile':'Chile','portugal':'Portugal','romania':'Romania'
|
||||||
|
}
|
||||||
|
ENV=pathlib.Path.home()/'.hermes/env/codex_oauth_onboarding.env'; env={}
|
||||||
|
for line in ENV.read_text().splitlines():
|
||||||
|
line=line.strip()
|
||||||
|
if not line or line.startswith('#') or '=' not in line: continue
|
||||||
|
if line.startswith('export '): line=line[7:].strip()
|
||||||
|
k,v=line.split('=',1); k=k.strip(); v=v.strip()
|
||||||
|
if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$',k):
|
||||||
|
try:
|
||||||
|
if v and v[0] in '"\'': v=shlex.split(v)[0]
|
||||||
|
except Exception: pass
|
||||||
|
env[k]=v.strip('"\'')
|
||||||
|
PHONE_SMS_PROVIDER=os.environ.get('PHONE_SMS_PROVIDER') or env.get('PHONE_SMS_PROVIDER','5sim').lower()
|
||||||
|
FIVE_SIM_KEY=env.get('FIVE_SIM_API_KEY')
|
||||||
|
LUBAN_KEY=env.get('LUBAN_SMS_API_KEY') or env.get('LUBAN_API_KEY')
|
||||||
|
LUBAN_BASE=(env.get('LUBAN_SMS_API_BASE') or 'https://lubansms.com/v2/api').rstrip('/')
|
||||||
|
LUBAN_SERVICE_QUERY=env.get('LUBAN_SMS_SERVICE_QUERY','OpenAI')
|
||||||
|
LUBAN_LANGUAGE=env.get('LUBAN_SMS_LANGUAGE','en')
|
||||||
|
if PHONE_SMS_PROVIDER == 'luban' and not LUBAN_KEY:
|
||||||
|
raise SystemExit('PHONE_SMS_PROVIDER=luban but LUBAN_SMS_API_KEY missing')
|
||||||
|
if PHONE_SMS_PROVIDER != 'luban' and not FIVE_SIM_KEY:
|
||||||
|
raise SystemExit('FIVE_SIM_API_KEY missing')
|
||||||
|
STATE_FILE=pathlib.Path('/tmp/phone_verify_until_success_state.json')
|
||||||
|
|
||||||
|
def log(obj): obj.setdefault('ts', int(time.time())); print(json.dumps(obj,ensure_ascii=False), flush=True)
|
||||||
|
def save_state(**kw):
|
||||||
|
cur={}
|
||||||
|
if STATE_FILE.exists():
|
||||||
|
try: cur=json.loads(STATE_FILE.read_text())
|
||||||
|
except Exception: cur={}
|
||||||
|
cur.update(kw); STATE_FILE.write_text(json.dumps(cur,ensure_ascii=False,indent=2))
|
||||||
|
def strict_code(text):
|
||||||
|
m=re.search(r'(?<!\\d)(\\d{6})(?!\\d)', text or '')
|
||||||
|
return m.group(1) if m else None
|
||||||
|
|
||||||
|
def http_json(url, headers=None, timeout=30):
|
||||||
|
req=urllib.request.Request(url,headers=headers or {'Accept':'application/json'})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req,timeout=timeout) as r: txt=r.read().decode(); st=r.status
|
||||||
|
except urllib.error.HTTPError as e: txt=e.read().decode(errors='ignore'); st=e.code
|
||||||
|
except Exception as e: return 0, {'error':type(e).__name__+': '+str(e)}
|
||||||
|
try: data=json.loads(txt)
|
||||||
|
except Exception: data={'raw':txt[:300]}
|
||||||
|
return st,data
|
||||||
|
|
||||||
|
def fivesim(path):
|
||||||
|
return http_json('https://5sim.net/v1'+path, headers={'Authorization':'Bearer '+FIVE_SIM_KEY,'Accept':'application/json'})
|
||||||
|
def luban(endpoint, **params):
|
||||||
|
params={k:v for k,v in params.items() if v is not None}; params['apikey']=LUBAN_KEY
|
||||||
|
url=LUBAN_BASE+'/'+endpoint.lstrip('/')+'?'+urllib.parse.urlencode(params)
|
||||||
|
return http_json(url, headers={'Accept':'application/json','User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36'})
|
||||||
|
def luban_services(country=None, service=None, page=1):
|
||||||
|
return luban('List', country=country, service=service or LUBAN_SERVICE_QUERY, language=LUBAN_LANGUAGE, page=page)
|
||||||
|
def luban_resolve_service(country_en=None):
|
||||||
|
candidates=[]
|
||||||
|
for page in range(1,4):
|
||||||
|
st,data=luban_services(country_en, LUBAN_SERVICE_QUERY, page)
|
||||||
|
if st==200 and data.get('code')==0 and isinstance(data.get('msg'),list):
|
||||||
|
for r in data['msg']:
|
||||||
|
if not isinstance(r,dict) or not r.get('service_id'): continue
|
||||||
|
name=str(r.get('service_name') or '')
|
||||||
|
ctry=str(r.get('country_name_en') or '')
|
||||||
|
if LUBAN_SERVICE_QUERY.lower() in name.lower() or 'openai' in name.lower() or 'chatgpt' in name.lower():
|
||||||
|
candidates.append(r)
|
||||||
|
if not candidates: return None
|
||||||
|
def cost(r):
|
||||||
|
try: return float(r.get('cost'))
|
||||||
|
except Exception: return 999999.0
|
||||||
|
return sorted(candidates,key=cost)[0]
|
||||||
|
def luban_buy(country_en=None):
|
||||||
|
svc=luban_resolve_service(country_en)
|
||||||
|
if not svc: return 200, {'code':400,'msg':'no_luban_service'}
|
||||||
|
st,data=luban('getNumber', service_id=svc['service_id'])
|
||||||
|
if isinstance(data,dict): data['_service']=svc
|
||||||
|
return st,data
|
||||||
|
|
||||||
|
def sms_buy(country):
|
||||||
|
if PHONE_SMS_PROVIDER == 'luban': return luban_buy(FIVESIM_TO_LUBAN_COUNTRY.get(country,country))
|
||||||
|
return fivesim(f'/user/buy/activation/{country}/any/openai')
|
||||||
|
def sms_check(act):
|
||||||
|
if PHONE_SMS_PROVIDER == 'luban': return luban('getSms', request_id=act)
|
||||||
|
return fivesim(f'/user/check/{act}')
|
||||||
|
def sms_cancel(act):
|
||||||
|
if PHONE_SMS_PROVIDER == 'luban': return luban('setStatus', request_id=act, status='reject')
|
||||||
|
return fivesim(f'/user/cancel/{act}')
|
||||||
|
def sms_finish(act):
|
||||||
|
if PHONE_SMS_PROVIDER == 'luban': return 200, {'code':0,'msg':'success'}
|
||||||
|
return fivesim(f'/user/finish/{act}')
|
||||||
|
def cdp():
|
||||||
|
tabs=json.load(urllib.request.urlopen(CDP+'/json/list', timeout=10))
|
||||||
|
pages=[t for t in tabs if t.get('type')=='page' and 'auth.openai.com' in t.get('url','')] or [t for t in tabs if t.get('type')=='page']
|
||||||
|
page=pages[0]
|
||||||
|
ws=websocket.create_connection(page['webSocketDebuggerUrl'], timeout=10, suppress_origin=True); c=itertools.count(1)
|
||||||
|
def call(m,p=None,timeout=30):
|
||||||
|
i=next(c); ws.send(json.dumps({'id':i,'method':m,'params':p or {}})); end=time.time()+timeout
|
||||||
|
while time.time()<end:
|
||||||
|
msg=json.loads(ws.recv())
|
||||||
|
if msg.get('id')==i:
|
||||||
|
if 'error' in msg: raise RuntimeError(msg['error'])
|
||||||
|
return msg.get('result')
|
||||||
|
raise TimeoutError(m)
|
||||||
|
def ev(e,timeout=90): return call('Runtime.evaluate',{'expression':e,'returnByValue':True,'awaitPromise':True},timeout).get('result',{})
|
||||||
|
call('Runtime.enable'); return ws,call,ev
|
||||||
|
def navigate(url, wait=6):
|
||||||
|
ws,call,ev=cdp()
|
||||||
|
try: call('Page.enable'); call('Page.navigate',{'url':url}); time.sleep(wait)
|
||||||
|
finally:
|
||||||
|
try: ws.close()
|
||||||
|
except Exception: pass
|
||||||
|
def page_state():
|
||||||
|
ws,call,ev=cdp()
|
||||||
|
try: return ev("({href:location.href,title:document.title,body:(document.body.innerText||'').slice(0,1500)})").get('value')
|
||||||
|
finally:
|
||||||
|
try: ws.close()
|
||||||
|
except Exception: pass
|
||||||
|
def ensure_add_phone():
|
||||||
|
st=page_state() or {}; href=st.get('href',''); body=st.get('body','')
|
||||||
|
if 'account_deactivated' in body.lower() or 'deactivated' in body.lower(): log({'phase':'account_deactivated_stop','href':href}); sys.exit(4)
|
||||||
|
if 'add-phone' in href: return True
|
||||||
|
navigate('https://auth.openai.com/add-phone', 8)
|
||||||
|
st=page_state() or {}; ok='add-phone' in st.get('href','')
|
||||||
|
if not ok: log({'phase':'recover_add_phone_fail','state':st})
|
||||||
|
return ok
|
||||||
|
def select_country(iso, cname):
|
||||||
|
if not ensure_add_phone(): return False, {'error':'not_add_phone'}
|
||||||
|
ws,call,ev=cdp()
|
||||||
|
try:
|
||||||
|
gate=ev(f"""(async()=>{{const sel=document.querySelector('select'); if(sel){{sel.value={json.dumps(iso)}; sel.dispatchEvent(new Event('input',{{bubbles:true}})); sel.dispatchEvent(new Event('change',{{bubbles:true}})); await new Promise(r=>setTimeout(r,900));}} const tel=document.querySelector('input[type=tel]'); if(tel) tel.focus(); return {{href:location.href,countryValue:sel?.value,countryText:sel?.options?.[sel.selectedIndex]?.text,body:(document.body.innerText||'').slice(0,1200)}};}})()""").get('value')
|
||||||
|
for typ,key,code,wvc in [('keyDown','a','KeyA',65),('keyUp','a','KeyA',65),('keyDown','Backspace','Backspace',8),('keyUp','Backspace','Backspace',8)]:
|
||||||
|
call('Input.dispatchKeyEvent',{'type':typ,'key':key,'code':code,'windowsVirtualKeyCode':wvc,'nativeVirtualKeyCode':wvc,'modifiers':2 if key=='a' else 0})
|
||||||
|
finally:
|
||||||
|
try: ws.close()
|
||||||
|
except Exception: pass
|
||||||
|
ok=gate and 'add-phone' in gate.get('href','') and gate.get('countryValue')==iso and cname in (gate.get('body','')+gate.get('countryText',''))
|
||||||
|
return ok,gate
|
||||||
|
def submit_phone(iso,phone):
|
||||||
|
digits=re.sub(r'\D','',str(phone)); dial=next((d for _,i,_,d in COUNTRIES if i==iso),''); local=digits
|
||||||
|
if dial and local.startswith(dial): local=local[len(dial):]
|
||||||
|
if local.startswith('0'): local=local[1:]
|
||||||
|
ws,call,ev=cdp()
|
||||||
|
try:
|
||||||
|
ev("document.querySelector('input[type=tel]')?.focus()"); call('Input.insertText',{'text':local}); time.sleep(.2)
|
||||||
|
return ev("""(async()=>{const sel=document.querySelector('select'); const tel=document.querySelector('input[type=tel]'); const btn=[...document.querySelectorAll('button')].find(b=>/继续|Continue|Next/.test(b.innerText)); const before={countryValue:sel?.value,countryText:sel?.options?.[sel.selectedIndex]?.text,tel_len:tel?.value?.length}; btn?.click(); await new Promise(r=>setTimeout(r,8500)); return {before,href:location.href,title:document.title,body:(document.body.innerText||'').slice(0,2200)};})()""",140).get('value')
|
||||||
|
finally:
|
||||||
|
try: ws.close()
|
||||||
|
except Exception: pass
|
||||||
|
def classify_submit(res):
|
||||||
|
body=res.get('body','') if res else ''; href=res.get('href','') if res else ''
|
||||||
|
rejected=any(x in body for x in ['无法向此电话号码发送验证码','电话号码无效']) or any(x in body.lower() for x in ['invalid phone','unable to send','try again later'])
|
||||||
|
sms_page=('phone-verification' in href) or ('验证码' in body and '重新发送' in body and not rejected)
|
||||||
|
deact='account_deactivated' in body.lower() or 'deactivated' in body.lower()
|
||||||
|
return rejected,sms_page,deact
|
||||||
|
def poll_sms(act):
|
||||||
|
end=time.time()+POLL_SECONDS; last={}
|
||||||
|
while time.time()<end:
|
||||||
|
st,data=sms_check(act); last=data if isinstance(data,dict) else {}
|
||||||
|
if PHONE_SMS_PROVIDER == 'luban':
|
||||||
|
msg=str(last.get('msg','')).lower()
|
||||||
|
if msg == 'success' and last.get('sms_code'):
|
||||||
|
return str(last.get('sms_code')), last
|
||||||
|
code=strict_code(json.dumps(last,ensure_ascii=False))
|
||||||
|
if code: return code,last
|
||||||
|
sms=last.get('sms') or []
|
||||||
|
if sms:
|
||||||
|
text=' '.join(str(x) for item in sms for x in (item.values() if isinstance(item,dict) else [item]))
|
||||||
|
m=re.search(r'(?<!\d)(\d{4,8})(?!\d)', text); return m.group(1) if m else 'SMS_PRESENT_NO_CODE', last
|
||||||
|
time.sleep(5)
|
||||||
|
return None,last
|
||||||
|
def submit_code(code):
|
||||||
|
ws,call,ev=cdp()
|
||||||
|
try:
|
||||||
|
ev("document.querySelector('input[name=code],input[autocomplete=one-time-code],input[type=text]')?.focus()"); call('Input.insertText',{'text':code}); time.sleep(.2)
|
||||||
|
return ev("""(async()=>{const btn=[...document.querySelectorAll('button')].find(b=>/继续|Continue|Next|验证|Verify/.test(b.innerText)); btn?.click(); await new Promise(r=>setTimeout(r,12000)); return {href:location.href,title:document.title,body:(document.body.innerText||'').slice(0,2000)};})()""",150).get('value')
|
||||||
|
finally:
|
||||||
|
try: ws.close()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
new_total=0; rejected_total=0; sms0_total=0; nofree_count=0; country_counts={c:0 for c,_,_,_ in COUNTRIES}; round_no=0
|
||||||
|
log({'phase':'until_success_start','start_used_numbers':START_USED,'max_new_numbers':MAX_NEW_NUMBERS or 'unlimited','poll_seconds':POLL_SECONDS})
|
||||||
|
while True:
|
||||||
|
round_no += 1
|
||||||
|
for country,iso,cname,dial in COUNTRIES:
|
||||||
|
for _ in range(PER_COUNTRY_ROUND):
|
||||||
|
if MAX_NEW_NUMBERS and new_total >= MAX_NEW_NUMBERS:
|
||||||
|
log({'phase':'safety_max_reached_no_success','start_used_numbers':START_USED,'new_numbers':new_total,'total_numbers':START_USED+new_total,'rejected_total':rejected_total,'sms0_total':sms0_total,'nofree_count':nofree_count,'counts':country_counts}); sys.exit(3)
|
||||||
|
ok,gate=select_country(iso,cname)
|
||||||
|
if not ok: log({'phase':'country_gate_fail','country':country,'iso':iso,'gate':gate}); break
|
||||||
|
st,data=sms_buy(country)
|
||||||
|
act=(data.get('request_id') or data.get('id')) if isinstance(data,dict) else None; phone=(data.get('number') or data.get('phone')) if isinstance(data,dict) else None
|
||||||
|
if not(act and phone):
|
||||||
|
nofree_count+=1; body=(data.get('raw') or data.get('error') or str(data))[:120] if isinstance(data,dict) else str(data)[:120]
|
||||||
|
log({'phase':'buy_no_activation','country':country,'http':st,'body':body,'nofree_count':nofree_count}); break
|
||||||
|
new_total+=1; country_counts[country]+=1; save_state(current_activation_id=act,new_numbers=new_total,total_numbers=START_USED+new_total,country=country); pathlib.Path('/tmp/current_fivesim_activation_id.txt').write_text(str(act))
|
||||||
|
log({'phase':'activation_bought','new_numbers':new_total,'total_numbers':START_USED+new_total,'country':country,'activation_id':act})
|
||||||
|
try: res=submit_phone(iso,phone)
|
||||||
|
except Exception as e:
|
||||||
|
st2,_=sms_cancel(act); log({'phase':'submit_exception_cancel','activation_id':act,'error':type(e).__name__,'http':st2,'ok':200<=st2<300}); navigate('https://auth.openai.com/add-phone',6); continue
|
||||||
|
rejected,sms_page,deact=classify_submit(res)
|
||||||
|
log({'phase':'phone_submitted','new_numbers':new_total,'total_numbers':START_USED+new_total,'country':country,'activation_id':act,'href':res.get('href') if res else None,'rejected':rejected,'sms_page':sms_page,'deactivated':deact})
|
||||||
|
if deact: sms_cancel(act); log({'phase':'account_deactivated_stop','activation_id':act,'total_numbers':START_USED+new_total}); sys.exit(4)
|
||||||
|
if rejected:
|
||||||
|
rejected_total+=1; st2,_=sms_cancel(act); log({'phase':'activation_cancel_rejected','activation_id':act,'http':st2,'ok':200<=st2<300}); continue
|
||||||
|
code,sdata=poll_sms(act)
|
||||||
|
if code and code!='SMS_PRESENT_NO_CODE':
|
||||||
|
pathlib.Path('/tmp/current_phone_sms_code.txt').write_text(code); log({'phase':'sms_received','new_numbers':new_total,'total_numbers':START_USED+new_total,'country':country,'activation_id':act})
|
||||||
|
res2=submit_code(code); log({'phase':'sms_code_submitted','href':res2.get('href'),'title':res2.get('title'),'total_numbers':START_USED+new_total}); sms_finish(act); save_state(success=True,success_activation_id=act,total_numbers=START_USED+new_total,country=country,provider=PHONE_SMS_PROVIDER,href=res2.get('href')); sys.exit(0)
|
||||||
|
sms0_total+=1; st2,_=sms_cancel(act); log({'phase':'sms_timeout_cancel','new_numbers':new_total,'total_numbers':START_USED+new_total,'country':country,'activation_id':act,'http':st2,'ok':200<=st2<300}); navigate('https://auth.openai.com/add-phone',6)
|
||||||
|
log({'phase':'round_done_no_success','round':round_no,'new_numbers':new_total,'total_numbers':START_USED+new_total,'rejected_total':rejected_total,'sms0_total':sms0_total,'nofree_count':nofree_count,'counts':country_counts})
|
||||||
Reference in New Issue
Block a user