#!/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'(?{{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(){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})