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=
'';
}
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('')+'
'+
''+
'购买和升级重试总并发
正在请求 '+replayRunning+' · 等待名额 '+replayWaiting+''+replayKindText(st.replay_by_kind)+'
购买、升级和加时打出去的请求加在一起的上限,包含单次购买和并发重试。默认 100,保存后马上生效,不用重启。用来避免本地网卡被打满。
'+
''+
''
+'
DroidGuard
正在检查'
+'
新登录的 minute_maid 密文从这台设备上的 com.google.android.gms.ui 读取。地址是 adb 序列号,真机用它的序列号,MuMu 默认 127.0.0.1:7555。已经拿到 master token 的账号不使用这里。
'
+'
'
+'
正在检查
'
+'
登录和加卡两份密文按这个间隔重新取,默认 10 分钟。超过这个时间的加卡密文会在绑卡前现取。保存后下一次计时按新间隔走,不用重启。
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'- 设备要有官方 com.google.android.gms,并打开 USB 调试。真机用数据线连上,模拟器先让 adb 连上它的端口。
'
+'- 把服务地址填成这台设备的 adb 序列号,点保存。
'
+'- 在设备上打开设置,进入账号,添加 Google 账号,停在登录页。这一步会把官方 DroidGuard 拉起来。
'
+'- 下载 Frida 脚本后,复制下面的命令执行。它按进程名 com.google.android.gms.ui 附加。
'
+'
'+'';
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;
}