function formatPAN(pan){ const d=String(pan||'').replace(/\D/g,''); if(!d) return ''; return d.replace(/(\d{4})(?=\d)/g,'$1 '); } function cardPAN(c){ return formatPAN(c.pan) || c.last4 || ''; } function visibleCards(){ return (cache.cards||[]).filter(c=>!c.hidden); } function cardLabel(c){ return (c.brand||c.scheme||'卡')+' '+cardPAN(c)+' '+String(c.exp_month).padStart(2,'0')+'/'+String(c.exp_year).slice(-2)+(c.bank?(' '+c.bank):''); } async function bind(ev,id){ const btn=clickBtn(ev); const acc=(cache.accounts||[]).find(a=>a && a.id===id) || {}; if(!(cache.cards||[]).length){ const cards=await api('/api/cards'); if(Array.isArray(cards)) cache.cards=cards.filter(Boolean); } const cards=visibleCards(); const opts=cards.map(c=>'').join(''); const loginProxy=String(acc.effective_proxy||acc.proxy||meta.default_proxy||'').trim(); let groups=[]; try{ const st=await api('/api/settings'); groups=(st.proxy_groups||[]).filter(g=>g && (g.proxies||[]).length); }catch(e){} const groupOpts=groups.map(g=>'').join(''); const root=$('#modalRoot'); root.innerHTML=''; const finish=v=>{ root.innerHTML=''; return v; }; bindMaskClose(root, ()=>finish(false)); const sel=$('#bindVault'); const triple=$('#bindTriple'); if(triple){ const fillTriple=()=>{ const got=parseCardTriple(triple.value); if(!got) return; $('#bindPan').value=got.pan; $('#bindExp').value=got.exp; $('#bindCvc').value=got.cvc; }; triple.addEventListener('input', fillTriple); triple.addEventListener('paste', ()=>setTimeout(fillTriple,0)); } const sync=()=>{ const on=!!sel.value; $('#bindPan').disabled=on; $('#bindExp').disabled=on; $('#bindCvc').disabled=on; if(triple) triple.disabled=on; }; sel.onchange=sync; sync(); const useLogin=$('#bindUseLogin'); if(useLogin) useLogin.onclick=()=>{ $('#bindProxy').value=loginProxy; }; const countryRows=()=>{ const g=groups.find(x=>(x.name||'')===(($('#bindPool')||{}).value||'')); const lines=(g&&g.proxies||[]).join('\n'); const counts=proxyCountryCounts(lines); return Object.keys(counts).filter(k=>k!=='未识别').sort().map(k=>({cc:k,n:counts[k]})); }; const paintCountries=()=>{ const rows=countryRows(); const q=String(($('#bindCCFilter')||{}).value||'').trim().toUpperCase(); const shown=q?rows.filter(r=>r.cc.indexOf(q)===0||r.cc.indexOf(q)>=0):rows; const box=$('#bindCC'); const current=box.dataset.cc||''; const pick=shown.some(r=>r.cc===current)?current:(shown[0]?shown[0].cc:''); box.dataset.cc=pick; box.innerHTML=shown.map(r=>'').join('') || '
没有匹配的国家
'; box.querySelectorAll('[data-cc]').forEach(btn=>{ btn.onclick=()=>{ box.dataset.cc=btn.getAttribute('data-cc'); paintCountries(); }; }); $('#bindPoolHint').textContent=rows.length?(shown.length+' / '+rows.length+' 个国家,输入代码即可定位'):'代理用户名里没有 country-xx 或 region-xx'; }; const fillCountries=()=>{ const f=$('#bindCCFilter'); if(f) f.value=''; paintCountries(); }; $('#bindPool').onchange=fillCountries; $('#bindCCFilter').addEventListener('input', paintCountries); $('#bindProxyMode').onchange=()=>{ const pool=$('#bindProxyMode').value==='pool'; $('#bindPoolBox').hidden=!pool; $('#bindManualBox').hidden=pool; }; fillCountries(); const done=await new Promise(resolve=>{ $('#mOk').onclick=()=>resolve(true); $('#mCancel').onclick=()=>resolve(false); }); if(!done){ finish(); return; } const okBtn=$('#mOk'); applyBtnBusy(okBtn, '检测出口'); let proxy=''; if($('#bindProxyMode').value==='pool'){ const g=($('#bindPool')||{}).value||''; const cc=String(($('#bindCC')||{}).dataset.cc||'').trim().toUpperCase(); if(!g){ unmarkBtnBusy('', okBtn); return fail('无法绑卡','先选一个代理组'); } if(!/^[A-Z]{2}$/.test(cc)){ unmarkBtnBusy('', okBtn); return fail('无法绑卡','先选出口国家代码'); } const via=$('#bindPoolVia') && $('#bindPoolVia').checked; proxy='pool:'+g+'|cc='+cc+'|egress='+(via?(meta.default_proxy||'http://127.0.0.1:10808'):'direct'); }else proxy=$('#bindProxy').value.trim(); if(!proxy || /^direct$/i.test(proxy) || proxy==='-'){ unmarkBtnBusy('', okBtn); return fail('无法绑卡','必须填写代理 IP,绑卡出口会改账号国家'); } const cardId=Number(sel.value||0); let body={proxy, mode: (($('input[name=bindMode]:checked')||{}).value||'wallet')}; if(cardId){ body.card_id=cardId; } else { const pan=$('#bindPan').value.trim(); const exp=$('#bindExp').value.trim(); const cvc=$('#bindCvc').value.trim(); if(!pan){ unmarkBtnBusy('', okBtn); return fail('无法绑卡','请选择已保存的卡,或填写卡号'); } const parts=(exp||'').split('/'); const mm=Number(parts[0]); let yy=Number(parts[1]); if(yy<100) yy+=2000; body.pan=pan; body.exp_month=mm; body.exp_year=yy; body.cvc=cvc; } $('#bindGeoHint').textContent='正在检测出口 IP 归属地…'; const geo=await api('/api/accounts/'+id+'/egress',{method:'POST',body:JSON.stringify({proxy})}); if(geo.error){ unmarkBtnBusy('', okBtn); $('#bindGeoHint').textContent=geo.error; return fail('无法检测出口', geo.error); } const eg=geo.egress||{}; const hits=(eg.hits||[]).map(h=>h.error? (h.source+': 失败') : (h.source+' '+((h.country||'-')+(h.city?('/'+h.city):'')))).join(';'); const accCC=String(geo.account_country||acc.play_country||'').toUpperCase(); const outCC=String(eg.country||'').toUpperCase(); const place=[outCC, eg.region, eg.city].filter(Boolean).join(' / ') || '未知'; const mismatch=!!geo.mismatch || (accCC && outCC && accCC!==outCC); $('#bindGeoHint').textContent='出口 '+((eg.ip||'-')+' · '+place); body.proxy=proxy; body.egress_ip=eg.ip||''; body.egress_country=outCC; body.egress_city=eg.city||''; body.egress_region=eg.region||''; let msg='当前出口 IP '+(eg.ip||'-')+',归属地 '+place+(eg.agree?('('+eg.agree+' 家接口一致)'):'')+'。\n探测:'+hits; if(mismatch){ msg='账号已有绑卡国家 '+countryLabel(accCC)+',本次出口是 '+countryLabel(outCC)+'。不一致会改账号地区,请确认是否继续。\n\n'+msg; } const ok=await modal({ stack:true, title: mismatch ? '出口国家与账号不一致' : '确认绑卡出口', message: msg, okText: mismatch ? '仍要绑定' : '确认并绑定', cancelText: '取消' }); if(!ok){ unmarkBtnBusy('', okBtn); return; } finish(); const j=await api('/api/accounts/'+id+'/instruments',{method:'POST',body:JSON.stringify(body)}); if(j.error) return fail('绑卡失败', j.error); watchAccountJob(id, 'bind', j.job_id, '绑卡'); } async function redeemGift(ev,id){ const acc=(cache.accounts||[]).find(a=>a && a.id===id) || {}; const root=$('#modalRoot'); root.innerHTML=''; const close=()=>{ document.removeEventListener('keydown', onKey); root.innerHTML=''; }; const onKey=e=>{ if(e.key==='Escape'){ e.preventDefault(); close(); } }; document.addEventListener('keydown', onKey); bindMaskClose(root, close); api('/api/accounts/'+id+'/balance').then(d=>{ const el=$('#redeemBal'); if(!el) return; el.textContent=(d&&d.play_balance)?d.play_balance:'未知'; }).catch(()=>{ const el=$('#redeemBal'); if(el) el.textContent='未知'; }); const input=$('#redeemCode'); if(input){ input.focus(); input.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); $('#mOk').click(); } }); } $('#mCancel').onclick=close; $('#mOk').onclick=async()=>{ const code=String(($('#redeemCode')||{}).value||'').trim(); if(!code){ $('#redeemOut').innerHTML='
还没有码先填入礼品卡码
'; return; } const okBtn=$('#mOk'); applyBtnBusy(okBtn, '兑换中'); $('#redeemOut').innerHTML='
正在向 Google 确认这张卡…
'; const j=await api('/api/accounts/'+id+'/redeem',{method:'POST',body:JSON.stringify({code})}); if(j.error){ unmarkBtnBusy('', okBtn); $('#redeemOut').innerHTML='
没有提交'+esc(j.error)+'
'; return; } const t0=Date.now(); while(Date.now()-t0<15*60*1000){ const job=await api('/api/jobs/'+j.job_id); const step=jobStepText(job); if(step) applyBtnBusy(okBtn, step); if(job && job.status==='challenge_required'){ unmarkBtnBusy('', okBtn); $('#redeemOut').innerHTML='
需要验证'+esc(job.challenge_hint||job.error||'请先完成账号验证')+'
'; return; } if(job && jobDone(job)){ unmarkBtnBusy('', okBtn); const raw=job.result; let res={}; if(typeof raw==='string'){ try{ res=JSON.parse(raw)||{}; }catch(e){ res={}; } } else if(raw && typeof raw==='object') res=raw; const ok=job.status==='succeeded'; const rows=[ ['礼品卡', res.code||j.code||code], ['到账账号', res.email||acc.email||''], ['金额', res.amount||''], ['结果', ok?'已兑换':(res.message||job.error||'失败')] ].filter(r=>r[1]); $('#redeemOut').innerHTML='
'+(ok?'兑换成功':'兑换失败')+''+ rows.map(r=>'
'+esc(r[0])+''+esc(r[1])+'
').join('')+ '
'; okBtn.textContent='再兑一张'; return; } await new Promise(r=>setTimeout(r, 1200)); } unmarkBtnBusy('', okBtn); $('#redeemOut').innerHTML='
仍在处理任务 #'+j.job_id+' 还没结束,可稍后在任务记录查看
'; }; } function offerKindName(k){ return ({trial:'试用', intro:'优惠价', promo:'促销', base:'原价', one_time:'一次性'})[k]||k||'方案'; } function offerIdentity(o){ return [o.offer_token,o.offer_id,o.base_plan_id,o.offer_kind,o.price,o.intro_price,o.billing_period,o.intro_period,o.intro_cycles].join('|'); } function offerPlanName(o){ return o.offer_id||o.base_plan_id||''; } function offerLine(o){ const kind=offerKindName(o.offer_kind); const dur=offerDuration(o); const plan=offerPlanName(o); return kind+' · '+offerPrice(o)+(dur?(' · '+dur):'')+(plan?(' · '+plan):''); } let fxRates=null; function fxMoney(amount, digits){ const n=Number(amount); if(!isFinite(n)) return '-'; return n.toLocaleString('zh-CN', {minimumFractionDigits:digits, maximumFractionDigits:digits}); } function fxOf(o){ if(!fxRates || !o) return null; const cur=String(o.currency||'').toUpperCase(); const per=fxRates[cur]; const micros=Number(o.price_amount_micros||0); if(!per || !micros) return null; const major=micros/1e6; return {cny:major*per.cny, usd:major*per.usd}; } function fxText(v){ if(!v) return ''; return '
¥'+fxMoney(v.cny, 2)+' · $'+fxMoney(v.usd, 2)+'
'; } function skuCard(s){ const offs=(s.offers||[]).filter((o,i,all)=>{ const key=offerIdentity(o); return all.findIndex(x=>offerIdentity(x)===key && String(x.country||'')===String(o.country||''))===i; }); let kids=''; if(!offs.length){ kids='
暂无报价
'; }else{ const plans={}; offs.forEach(o=>{ const k=o.base_plan_id||'方案'; (plans[k]=plans[k]||[]).push(o); }); const byCountry=list=>{ const by={}; list.forEach(o=>{ const cc=String(o.country||'').toUpperCase(); if(!/^[A-Z]{2}$/.test(cc) || cc==='XX') return; if(!by[cc]) by[cc]=o; }); return by; }; const planNames=Object.keys(plans).sort(); const tabs='
'+planNames.map((plan,i)=>'').join('')+'
'; kids=tabs+planNames.map((plan,i)=>{ const list=plans[plan]; const base=byCountry(list.filter(o=>!o.offer_id)); const offers={}; list.filter(o=>o.offer_id).forEach(o=>{ (offers[o.offer_id]=offers[o.offer_id]||[]).push(o); }); const offerIds=Object.keys(offers).sort(); const ccs=[...new Set(Object.keys(base).concat(offerIds.flatMap(id=>Object.keys(byCountry(offers[id])))))]; ccs.sort((a,b)=>{ const av=fxOf(base[a]); const bv=fxOf(base[b]); if(av && bv && av.cny!==bv.cny) return av.cny-bv.cny; if(av && !bv) return -1; if(!av && bv) return 1; return a.localeCompare(b); }); const head='国家计费周期方案价格'+offerIds.map(id=>''+esc(id)+'
'+esc(offerKindName((offers[id][0]||{}).offer_kind))+'
').join(''); const rows=ccs.map(cc=>{ const b=base[cc]; const cells=offerIds.map(id=>{ const o=(byCountry(offers[id]))[cc]; if(!o) return '-'; const intro=o.intro_price && o.intro_price!==o.price; return ''+(intro?esc(o.intro_price)+'
之后 '+esc(offerPrice(o))+'
':esc(offerPrice(o)))+fxText(fxOf(o))+''; }).join(''); return ''+esc(countryLabel(cc))+''+esc(offerDuration(b||list[0])||'-')+''+(b?esc(offerPrice(b))+fxText(fxOf(b)):'-')+''+cells+''; }).join(''); return '
'+head+''+rows+'
'; }).join(''); } const retired=String(s.note||'').indexOf('作废')>=0; const name=retired?''+esc(s.title||s.sku)+'':esc(s.title||s.sku); const sub=retired?''+esc(s.sku)+' · '+esc(s.product_type||'')+'':esc(s.sku)+' · '+esc(s.product_type||''); return '
'+name+'
'+sub+(s.note?(' · '+esc(s.note)):'')+'
'+kids+'
'; } function toggleSkuPrices(id, btn){ const box=document.getElementById('skuPrices'+id); if(!box) return; box.hidden=!box.hidden; if(btn) btn.classList.toggle('on', !box.hidden); } function appIdentity(pkg){ return (cache.apps||[]).find(a=>a.package===pkg)||null; } function skuPriceCountries(list){ const ccs=new Set(); (list||[]).forEach(s=>(s.offers||[]).forEach(o=>{ const cc=String(o.country||'').trim().toUpperCase(); if(/^[A-Z]{2}$/.test(cc) && cc!=='XX') ccs.add(cc); })); return [...ccs]; } function accountForCountry(cc){ const ready=(cache.accounts||[]).filter(a=>a && (a.status==='ready'||a.status==='authenticated')); return ready.find(a=>String(a.play_country||'').toUpperCase()===cc) || null; } async function refreshSkuPrice(pkg){ const all=(cache.skus||[]).filter(s=>s && s.package===pkg && s.sku); if(!all.length) return fail('无法更新','这个应用没有 SKU'); let ccs=skuPriceCountries(all); if(!ccs.length){ const any=accountForCountry('') || (cache.accounts||[]).find(a=>a && (a.status==='ready'||a.status==='authenticated')); const cc=String((any&&any.play_country)||'').toUpperCase(); if(!/^[A-Z]{2}$/.test(cc)) return fail('无法更新','没有已登录账号'); ccs=[cc]; } const byType={}; all.filter(s=>String(s.note||'').indexOf('作废')<0).forEach(s=>{ const t=s.product_type||'subs'; (byType[t]=byType[t]||[]).push(s.sku); }); const missing=ccs.filter(cc=>!accountForCountry(cc)); if(missing.length) notice('部分国家没有账号', missing.join('、'), 'err'); const todo=ccs.filter(cc=>accountForCountry(cc)); if(!todo.length) return fail('无法更新','这些国家都没有已登录账号'); notice('正在更新价格', pkg+' · '+todo.join('、')); let failed=0; for(const cc of todo){ const acc=accountForCountry(cc); for(const t of Object.keys(byType)){ const j=await api('/api/sku/details',{method:'POST',body:JSON.stringify({account_id:acc.id, package:pkg, product_type:t, skus:byType[t], refresh:true})}); if(j.error){ failed++; notice('无法更新', cc+' '+j.error, 'err'); continue; } const done=await waitJob(j.job_id, '更新价格 '+cc); if(!done || done.status!=='succeeded') failed++; } } await loadAll(); render(); if(!failed) notice('价格已更新', pkg+' · '+todo.join('、')); } async function refreshApp(pkg){ const ready=(cache.accounts||[]).find(a=>a.status==='ready'||a.status==='authenticated'); if(!ready) return fail('无法更新','需要一个已登录账号'); const j=await api('/api/apps/refresh',{method:'POST',body:JSON.stringify({account_id:ready.id, package:pkg})}); if(j.error) return fail('无法更新', j.error); notice('正在更新', pkg); await waitJob(j.job_id, '更新应用'); await loadAll(); render(); } function fxSummary(){ if(!fxRates) return '汇率未加载'; const cny=fxRates.USD&&fxRates.USD.cny; const when=fxRates._at?new Date(fxRates._at).toLocaleString('zh-CN',{hour12:false}):''; return '1 USD = ¥'+(cny?fxMoney(cny,4):'-')+(when?(' · '+when):''); } async function loadFx(){ try{ const saved=JSON.parse(localStorage.getItem('playpay_fx')||'null'); if(saved && saved.USD) fxRates=saved; }catch(e){} } async function refreshFx(){ notice('正在刷新汇率', ''); let data=null; try{ const res=await fetch('https://open.er-api.com/v6/latest/USD'); data=await res.json(); }catch(e){ return fail('无法刷新汇率', '网络不可用'); } const usd=data&&data.rates; if(!usd || !usd.CNY) return fail('无法刷新汇率', '没有返回汇率'); const rates={_at:Date.now()}; Object.keys(usd).forEach(cur=>{ const perUsd=Number(usd[cur]); if(perUsd>0) rates[cur]={usd:1/perUsd, cny:usd.CNY/perUsd}; }); rates.USD={usd:1, cny:Number(usd.CNY)}; rates.CNY={usd:1/Number(usd.CNY), cny:1}; fxRates=rates; localStorage.setItem('playpay_fx', JSON.stringify(rates)); notice('汇率已更新', fxSummary()); render(false); } function viewSkus(){ if(!fxRates) loadFx(); const pkgs=sortApps([...new Set((cache.skus||[]).map(s=>s.package).filter(Boolean))]); const skuRetired=s=>String(s&&s.note||'').indexOf('作废')>=0; const filtered=(cache.skus||[]).filter(s=>{ if(filters.skuPkg && s.package!==filters.skuPkg) return false; return matchQ([s.title,s.package,s.sku,s.product_type,s.note,String(s.id)].concat((s.offers||[]).map(o=>[o.country,o.price,o.intro_price,o.offer_kind].join(' ')))); }).slice().sort((a,b)=>(skuRetired(a)-skuRetired(b)) || ((b.offers||[]).length>0)-((a.offers||[]).length>0) || String(a.title||a.sku).localeCompare(String(b.title||b.sku),'zh')); const grouped={}; filtered.forEach(s=>{ const pkg=s.package||'(无包名)'; (grouped[pkg]=grouped[pkg]||[]).push(s); }); const pkgKeys=sortApps(Object.keys(grouped)); const info=paginate(pkgKeys,'skus'); const pageKeys=info.slice; const blocks=pageKeys.map(pkg=>{ const items=grouped[pkg]; const app=appIdentity(pkg); const ver=app&&Number(app.version_code)>0?('版本 '+app.version_code):'版本未更新'; const md5=app&&app.cert_md5?app.cert_md5:'签名未更新'; const sha=app&&app.cert_sha256?('
SHA256 '+esc(app.cert_sha256)+'
'):''; const tabs=items.map((s,i)=>{ const retired=String(s.note||'').indexOf('作废')>=0; return ''; }).join(''); const panes=items.map((s,i)=>'
'+skuCard(s)+'
').join(''); const ack=''; return '
'+esc(pkg)+' · '+items.length+' 个 SKU
'+esc(ver)+' · MD5 '+esc(md5)+'
'+sha+'
'+ack+'
'+tabs+'
'+panes+'
'; }).join(''); $('#app').innerHTML = '
'+ ''+ ''+ ''+fxSummary()+''+ '
'+ '
'+ filterSelect('包名', 'filters.skuPkg=this.value; pageNo.skus=1; render(false)', ''+pkgs.map(p=>'').join(''))+ '当前 '+filtered.length+' / '+(cache.skus||[]).length+''+ '
'+ (blocks||'
还没有 SKU。先添加,例如 ChatGPT Go。
')+ '
'+pager('skus', info); document.querySelectorAll('.sku-tab').forEach(btn=>{ btn.onclick=()=>{ const pkg=btn.closest('.sku-pkg'); pkg.querySelectorAll('.sku-tab').forEach(b=>b.classList.toggle('on', b===btn)); pkg.querySelectorAll('.sku-pane').forEach(p=>p.hidden=p.getAttribute('data-sku-pane')!==btn.getAttribute('data-sku-tab')); }; }); document.querySelectorAll('[data-auto-ack]').forEach(el=>{ el.onchange=async()=>{ const pkg=el.getAttribute('data-auto-ack'); const on=!!el.checked; const j=await api('/api/apps/auto-ack',{method:'POST',body:JSON.stringify({package:pkg, auto_ack:on})}); if(j.error){ el.checked=!on; return fail('无法保存', j.error); } const app=(cache.apps||[]).find(a=>a && a.package===pkg); if(app) app.auto_ack=on; else cache.apps=(cache.apps||[]).concat([{package:pkg, auto_ack:on}]); notice(on?'已开启自动确认':'已关闭自动确认', pkg); }; }); document.querySelectorAll('[data-plan-tab]').forEach(btn=>{ btn.onclick=()=>{ const box=btn.closest('.sku-kids'); box.querySelectorAll('[data-plan-tab]').forEach(b=>b.classList.toggle('on', b===btn)); box.querySelectorAll('[data-plan-pane]').forEach(p=>p.hidden=p.getAttribute('data-plan-pane')!==btn.getAttribute('data-plan-tab')); }; }); }