function queueLabel(t){ return ({login:'登录',keepalive:'保活',bind_instrument:'绑卡',redeem_gift:'兑换礼品卡',sku_details:'查 SKU',purchase:'购买',sub_renew:'升级',extend:'加时',sub_refresh:'刷新状态',sub_cancel:'取消订阅',sub_ack:'确认订阅'})[t]||typeName(t); } function paintProxies(){ const dock=document.getElementById('pagerDock'); if(dock) dock.innerHTML=''; const groups=window._proxyGroups||[]; if(window._proxyIdx==null || window._proxyIdx>=groups.length) window._proxyIdx=groups.length?0:-1; const i=window._proxyIdx; const list=groups.map(function(g,n){ const on=n===i?' on':''; return ''; }).join('')||'
还没有代理组
'; const g=i>=0?groups[i]:null; const editor=g?( '
'+ '
'+ ''+ ''+ ''+ ''+ '
'+ ''+ '
'+proxyCountryChips(g.text)+'
'+ '
' ):'
左侧添加一个代理组
'; $('#app').innerHTML= '
'+ '
代理组
'+list+'
'+ editor+ '
'; } async function viewProxies(){ window._proxyGroups=null; const st=await api('/api/settings'); window._proxyGroups=(st.proxy_groups||[]).map(g=>({name:g.name||'', enabled:g.enabled!==false, text:(g.proxies||[]).join(String.fromCharCode(10))})); paintProxies(); } async function saveProxyGroups(){ const proxy_groups=readProxyGroups().filter(g=>String(g.name||'').trim()||String(g.text||'').trim()).map(g=>({name:String(g.name||'').trim()||'未命名', enabled:true, proxies:proxyLines(g.text)})); const j=await api('/api/settings',{method:'PUT',body:JSON.stringify({proxy_groups})}); if(j.error) return fail('保存失败', j.error); notice('已保存', '代理池已更新'); const keep=window._proxyIdx; await viewProxies(); if(keep!=null && keep<(window._proxyGroups||[]).length){ window._proxyIdx=keep; paintProxies(); } } async function viewSettings(){ const dock=document.getElementById('pagerDock'); if(dock) dock.innerHTML=''; const st=await api('/api/settings'); const queues=st.queues||meta.queues||{}; const keys=['login','keepalive','bind_instrument','redeem_gift','sku_details','purchase','sub_renew','extend','sub_refresh','sub_cancel','sub_ack']; const qRun=st.queue_running||{}; const qWait=st.queue_waiting||{}; const rows=keys.map(k=>''+esc(queueLabel(k))+'
'+esc(k)+'
'+(Number(qRun[k])||0)+''+(Number(qWait[k])||0)+'').join(''); const replayCap=Number(st.replay_inflight||100); const replayRunning=Number(st.replay_running||0); const replayWaiting=Number(st.replay_waiting||0); const catalog=(st.country_catalog&&st.country_catalog.length)?st.country_catalog:[ {code:'US',name:'美国',fields:['持卡人','街道','城市','州','邮编']},{code:'HK',name:'香港',fields:['持卡人','街道','地区']}, {code:'SG',name:'新加坡',fields:['持卡人','街道','邮编']},{code:'JP',name:'日本',fields:['持卡人','丁目番地','邮编']}, {code:'GB',name:'英国',fields:['持卡人','街道','城市','邮编']},{code:'AR',name:'阿根廷',fields:['持卡人','街道','城市','省份','邮编']} ]; const enabled=new Set((st.billing_countries&&st.billing_countries.length)?st.billing_countries:catalog.map(c=>c.code)); const fieldText=c=>(c.fields||[]).join('、'); window._countryCatalog=catalog.map(c=>({code:c.code,name:c.name||c.code,fields:c.fields||[]})); const countries=window._countryCatalog.map((c,i)=>''+esc(c.code)+''+esc(c.name)+''+esc(fieldText(c))+' ').join(''); const tabs=[['queue','任务队列'],['replay','重试并发'],['chrome','Chrome'],['droidguard','DroidGuard']]; const tab=window._settingsTab||'queue'; window._settingsTab=tab; $('#app').innerHTML = '
'+tabs.map(t=>'').join('')+'
'+ '
不同类型任务使用独立队列,互不抢占。默认每队列 6 个并发,保存后立即生效,不用重启。
账号未单独设置代理时使用。保存在数据库,不读配置文件。
'+rows+'
队列并发数正在运行排队
'+ '

购买和升级重试总并发

正在请求 '+replayRunning+' · 等待名额 '+replayWaiting+'
'+replayKindText(st.replay_by_kind)+'
购买、升级和加时打出去的请求加在一起的上限,包含单次购买和并发重试。默认 100,保存后马上生效,不用重启。用来避免本地网卡被打满。
'+ '

Chrome

登录使用这个浏览器。路径留空或不存在时自动选择。
当前实际使用:'+esc(st.chrome_active||'没有找到')+'
只填 1 到 60 的整数,默认 20。登录页的时钟按这个倍数走,用来缩短 BotGuard 的等待。1 是正常速度,数字越大页面内的定时器越快。只影响登录,保存后立即生效,不用重启。
'+ '
' +'

DroidGuard

正在检查
' +'
新登录的 minute_maid 密文从这台设备上的 com.google.android.gms.ui 读取。地址是 adb 序列号,真机用它的序列号,MuMu 默认 127.0.0.1:7555。已经拿到 master token 的账号不使用这里。
' +'' +'
正在检查
' +'
登录和加卡两份密文按这个间隔重新取,默认 10 分钟。超过这个时间的加卡密文会在绑卡前现取。保存后下一次计时按新间隔走,不用重启。
' +'
' +'
' +'
' +'
' +'
' +'
' +'
' +'' +'
    ' +'
  1. 设备要有官方 com.google.android.gms,并打开 USB 调试。真机用数据线连上,模拟器先让 adb 连上它的端口。
  2. ' +'
  3. 把服务地址填成这台设备的 adb 序列号,点保存。
  4. ' +'
  5. 在设备上打开设置,进入账号,添加 Google 账号,停在登录页。这一步会把官方 DroidGuard 拉起来。
  6. ' +'
  7. 下载 Frida 脚本后,复制下面的命令执行。它按进程名 com.google.android.gms.ui 附加。
  8. ' +'
'+'
'; startReplayLive(); if(tab==='droidguard'){ refreshDgCmd(); loadLoginDeps(); loadDroidGuard(); } } function showSettingsTab(tab){ window._settingsTab=tab; if(tab==='droidguard'){ refreshDgCmd(); loadLoginDeps(); loadDroidGuard(); } document.querySelectorAll('[data-settings]').forEach(el=>{ el.hidden=el.getAttribute('data-settings')!==tab; }); document.querySelectorAll('#app .chips .btn').forEach(btn=>{ btn.classList.toggle('on', btn.getAttribute('onclick')===("showSettingsTab('"+tab+"')")); }); } let replayLiveTimer=0; function replayKindText(by){ by=by||{}; return '购买 '+((Number(by.purchase)||0))+' · 升级 '+((Number(by.sub_renew)||0))+' · 加时 '+((Number(by.extend)||0)); } function startReplayLive(){ if(replayLiveTimer) clearInterval(replayLiveTimer); replayLiveTimer=setInterval(async()=>{ if(page!=='settings' || !document.getElementById('replayLive')){ clearInterval(replayLiveTimer); replayLiveTimer=0; return; } const st=await api('/api/settings'); const el=document.getElementById('replayLive'); if(!el || st.error) return; el.textContent='正在请求 '+(Number(st.replay_running)||0)+' · 等待名额 '+(Number(st.replay_waiting)||0); const kinds=document.getElementById('replayKinds'); if(kinds) kinds.textContent=replayKindText(st.replay_by_kind); const run=st.queue_running||{}, wait=st.queue_waiting||{}; document.querySelectorAll('#queueRows tr[data-queue]').forEach(tr=>{ const k=tr.getAttribute('data-queue'); const a=tr.querySelector('.q-run'), b=tr.querySelector('.q-wait'); if(a) a.textContent=String(Number(run[k])||0); if(b) b.textContent=String(Number(wait[k])||0); }); }, 1000); } function dgCommand(){ const serial=((document.getElementById('dgSerial')||{}).value||'127.0.0.1:7555').trim(); return 'frida -D '+serial+' -n com.google.android.gms.ui -l dg_snap_value.js -q -t 20'; } function refreshDgCmd(){ const box=document.getElementById('dgFridaCmd'); if(box) box.value=dgCommand(); } async function copyDgCmd(){ const text=dgCommand(); try{ await navigator.clipboard.writeText(text); notice('\u5df2\u590d\u5236', text); }catch(e){ const box=document.getElementById('dgFridaCmd'); if(box){ box.focus(); box.select(); } notice('\u590d\u5236\u5931\u8d25', '\u8bf7\u624b\u52a8\u9009\u4e2d\u547d\u4ee4\u590d\u5236', 'err'); } } function downloadDgScript(){ downloadDistFile('script'); } function downloadDistFile(name){ const a=document.createElement('a'); a.href='/api/deps/login/file?name='+encodeURIComponent(name)+'&token='+encodeURIComponent(token||''); a.download=''; document.body.appendChild(a); a.click(); a.remove(); } function dgAge(seconds){ seconds=Math.max(0, Number(seconds)||0); if(seconds<60) return seconds+'\u79d2'; if(seconds<3600) return Math.floor(seconds/60)+'\u5206\u949f'; return Math.floor(seconds/3600)+'\u5c0f\u65f6'+Math.floor((seconds%3600)/60)+'\u5206\u949f'; } async function loadDroidGuard(){ ['dgRefresh','dgLoginRefresh'].forEach(function(id){ const b=document.getElementById(id); if(b) b.textContent='一键更新'; }); const loginLabel=document.getElementById('dgLoginLabel'); const bindLabel=document.getElementById('dgBindLabel'); if(loginLabel) loginLabel.textContent='登录 minute_maid'; if(bindLabel) bindLabel.textContent='加卡 acquire'; const st=await api('/api/deps/droidguard'); if(st.error){ return; } paintDG('dgLoginState','dgLoginInfo', st.login||{}); paintDG('dgState','dgPayload', st.bind||{}); const note=document.getElementById('dgGmsNotice'); if(note){ note.hidden=!st.gms_notice; note.textContent=st.gms_notice||''; } } function paintDG(stateId, infoId, blob){ const state=document.getElementById(stateId); const info=document.getElementById(infoId); if(!state) return; const stale=blob.state==='stale'; state.className='pill '+(blob.state==='fresh'?'st-ready':(stale?'st-token_expired':'st-disabled')); state.textContent=blob.state==='fresh'?'有效':(stale?'已过期':'没有密文'); if(info){ const when=blob.fetched_at?dgAge(blob.age_seconds)+'前更新':'还没有更新过'; info.textContent=(blob.present?(blob.bytes+' 字节 · '+(blob.prefix||'')+' · '):'')+when+(blob.error?' · 上次更新失败:'+blob.error:''); } } async function refreshDG(btnId, path){ const btn=document.getElementById(btnId); if(btn){ btn.disabled=true; btn.textContent='\u6b63\u5728\u66f4\u65b0'; } const st=await api(path,{method:'POST'}); if(btn) btn.disabled=false; if(st.error) notice('\u66f4\u65b0\u5931\u8d25', st.error, 'err'); else notice('\u5df2\u66f4\u65b0', (st.bytes||0)+' \u5b57\u8282'); loadDroidGuard(); } function showDgTip(kind){ const tips={ login:{title:'登录 minute_maid', message:'每次登录现取一份。程序用账号的 android id、SDK 和 GMS 版本算出材料,键是 dg_minutemaid,交给 com.google.android.gms.ui 里活着的 DroidGuard VM 签名。密文放进登录请求,用来过 Google 的设备校验。\n\n一键更新先打开设备的添加账号页,等列表里真出现 Google 再点它,把 gms.ui 拉到登录页。frida-server 在这之前先停掉,登录页起来后再重新拉起,否则 GMS 新进程会在启动时崩溃。拉不起来会直接提示,不再继续取。'}, bind:{title:'加卡 acquire', message:'加卡的 acquire 请求带这一份。同一份脚本、同一个 VM,键换成 flow,值是 phonesky_acquire_flow。取到后存进数据库,加卡时直接读,不每次现取。\n\n一键更新同样先拉起 gms.ui,再开 frida-server。账号类型列表里没有 Google 时不会点固定位置。登录页停住后才能签出结果。'} }; const tip=tips[kind]||tips.login; modal({title:tip.title, message:tip.message, cancelText:false, okText:'知道了'}); } async function recheckDgService(){ const btn=document.getElementById('dgRecheck'); if(btn){ btn.disabled=true; btn.textContent='检测中'; } await loadLoginDeps(); if(btn){ btn.disabled=false; btn.textContent='重新检测'; } } function refreshDroidGuard(){ return refreshDG('dgRefresh','/api/deps/droidguard/refresh'); } function refreshLoginDG(){ return refreshDG('dgLoginRefresh','/api/deps/droidguard/refresh-login'); } async function loadLoginDeps(){ const device=document.getElementById('dgDevice'); if(!device) return; const serial=(document.getElementById('dgSerial')||{}).value||''; const st=await api('/api/deps/login'+(serial?'?serial='+encodeURIComponent(serial):'')); refreshDgCmd(); if(st.error){ device.textContent=st.error; return; } device.textContent=(st.serial||'')+' · '+(st.device||''); paintServiceState(st); const input=document.getElementById('dgSerial'); if(input && document.activeElement!==input && st.serial) input.value=st.serial; } function paintServiceState(st){ const pill=document.getElementById('dgServiceState'); const ver=document.getElementById('dgGmsVersion'); if(!pill) return; const dev=String(st.device||''); let text='未连接', cls='st-disabled'; if(dev.indexOf('没有安装')>=0){ text='未安装 GMS'; cls='st-token_expired'; } else if(st.gms_running){ text='已连接'; cls='st-ready'; } pill.className='pill '+cls; pill.textContent=text; if(ver) ver.textContent=st.gms_version?('GMS '+st.gms_version):''; } async function editBillingCountry(index){ const cur=index>=0?(window._countryCatalog||[])[index]:null; const vals=await modal({title:cur?'修改国家':'添加国家', okText:'确定', fields:[ {label:'国家代码', value:cur?cur.code:''}, {label:'名称', value:cur?cur.name:''}, {label:'必填字段,用顿号或逗号分隔', value:cur?(cur.fields||[]).join('、'):'持卡人、街道、城市、地区、邮编'} ]}); if(!vals) return; const code=String(vals[0]||'').trim().toUpperCase(); const name=String(vals[1]||'').trim()||code; const fields=String(vals[2]||'').split(/[、,,]/).map(s=>s.trim()).filter(Boolean); if(!/^[A-Z]{2}$/.test(code)) return fail('无法保存','国家代码必须是两位字母'); const list=window._countryCatalog||[]; if(list.some((c,i)=>c.code===code && i!==index)) return fail('无法保存','国家已存在'); const row={code,name,fields}; if(index>=0) list[index]=row; else list.push(row); window._countryCatalog=list; saveSettings(); } function deleteBillingCountry(index){ const list=window._countryCatalog||[]; list.splice(index,1); window._countryCatalog=list; saveSettings(); } function proxyLines(text){ const nl=String.fromCharCode(10), cr=String.fromCharCode(13); return String(text||'').split(nl).map(function(s){return s.split(cr).join('').trim();}).filter(function(s){return s && s.charAt(0)!=='#';}); } function proxyCountryCounts(text){ const counts={}; proxyLines(text).forEach(function(line){ const m=line.match(/(?:country|region)[-_]([a-z]{2})/i); const cc=m?m[1].toUpperCase():'未识别'; counts[cc]=(counts[cc]||0)+1; }); return counts; } function proxyGroupSummary(text){ const counts=proxyCountryCounts(text); const n=proxyLines(text).length; const parts=Object.keys(counts).sort().map(function(k){return k+' '+counts[k];}); return n+' 条'+(parts.length?' · '+parts.join('、'):''); } function proxyCountryChips(text){ const counts=proxyCountryCounts(text); const keys=Object.keys(counts).sort(); if(!keys.length) return '还没有代理,国家会在粘贴后显示'; return keys.map(function(k){return ''+esc(k)+' '+counts[k]+'';}).join(''); } function refreshProxyCountries(){ const ta=document.querySelector('[data-proxy-text]'); const box=document.getElementById('proxyCC'); const item=document.querySelector('.proxy-item.on span'); const node=document.querySelector('[data-proxy-group]'); if(!ta) return; if(box) box.innerHTML=proxyCountryChips(ta.value); if(item) item.textContent=proxyLines(ta.value).length+' 条'; } function selectProxyGroup(i){ readProxyGroups(); window._proxyIdx=i; paintProxies(); } function addProxyGroup(){ readProxyGroups(); window._proxyGroups.push({name:'', text:''}); window._proxyIdx=window._proxyGroups.length-1; paintProxies(); const input=document.querySelector('[data-proxy-name]'); if(input) input.focus(); } function removeProxyGroup(i){ readProxyGroups(); window._proxyGroups.splice(i,1); if(window._proxyIdx>=window._proxyGroups.length) window._proxyIdx=window._proxyGroups.length-1; paintProxies(); } function readProxyGroups(){ const node=document.querySelector('[data-proxy-group]'); const groups=window._proxyGroups||[]; if(!node) return groups; const i=Number(node.getAttribute('data-proxy-group')); if(!groups[i]) return groups; groups[i]={ name:(node.querySelector('[data-proxy-name]')||{}).value||'', text:(node.querySelector('[data-proxy-text]')||{}).value||'' }; window._proxyGroups=groups; return groups; } async function saveSettings(){ const keys=['login','keepalive','bind_instrument','redeem_gift','sku_details','purchase','sub_renew','extend','sub_refresh','sub_cancel','sub_ack']; const queues={}; keys.forEach(k=>{ const n=Number((document.getElementById('q-'+k)||{}).value||6); queues[k]=n; }); const billing=[...document.querySelectorAll('#billingCountries input[data-cc]:checked')].map(el=>el.dataset.cc); const catalog=(window._countryCatalog||[]).map(c=>c.code); const chosen=catalog.filter(cc=>billing.includes(cc)); if(!chosen.length) return fail('无法保存','至少勾选一个国家'); const replayInflight=Math.max(1, Number((document.getElementById('replayInflight')||{}).value||100)); const chromePath=(document.getElementById('chromePath')||{}).value||''; const loginClockScale=Math.max(1, Math.min(60, Math.round(Number((document.getElementById('loginClockScale')||{}).value||20)))); const j=await api('/api/settings',{method:'PUT',body:JSON.stringify({queues, replay_inflight:replayInflight, billing_countries:chosen, country_catalog:window._countryCatalog||[], default_proxy:(document.getElementById('defaultProxy')||{}).value||'', proxy_enabled:!!((document.getElementById('proxyEnabled')||{}).checked), chrome_path:chromePath, login_clock_scale:loginClockScale, dg_serial:(document.getElementById('dgSerial')||{}).value||'', dg_refresh_minutes:Math.max(1, Math.min(1440, Math.round(Number((document.getElementById('dgRefreshMinutes')||{}).value||10))))})}); if(j.error) return fail('保存失败', j.error); const active=document.getElementById('chromeActive'); if(active) active.textContent='当前实际使用:'+(j.chrome_active||'没有找到'); notice('已保存', '当前实际使用:'+(j.chrome_active||'没有找到')); if(j.queues) meta.queues=j.queues; }