0?''+esc(extendPlanName(st.plan))+' · '+esc(extendModeName(st.mode))+'
':'起点
';
return ''+esc(shortSku(st.sku))+''+plan+'
';
});
return ''+bits.join('→')+'
';
}
function jobProgress(j){
const r=jobResult(j);
if(!r) return '';
if(r.step) return r.step;
if(r.sent && r.attempts) return '并发重试 '+r.sent+'/'+r.attempts;
return '';
}
function jobSummary(j){
const bits=[];
if(jobIsRotate(j)) bits.push(j.type==='login'?'轮换后重新登录':'重置设备身份并重新登录');
const rec=jobReceipt(j);
if(rec.sku) bits.push((rec.package?rec.package+' / ':'')+rec.sku);
const progress=jobProgress(j);
if(progress && (j.status==='running'||j.status==='queued')) bits.push(progress);
if(j.error) bits.push(j.error);
else if(j.challenge_type) bits.push('验证: '+j.challenge_type+(j.challenge_hint?(' / '+j.challenge_hint):''));
return bits.join(String.fromCharCode(10)) || '-';
}
function jobFailText(j){
const r=jobResult(j);
const fails=r&&r.failures;
if(!fails || typeof fails!=='object') return '';
return Object.keys(fails).map(k=>[k,Number(fails[k])||0]).filter(x=>x[1]>0).sort((a,b)=>b[1]-a[1]).map(x=>x[0]+' '+x[1]).join('\n');
}
function jobProgressLine(j){
const r=jobResult(j)||{};
const sent=Number(r.sent||0), attempts=Number(r.attempts||0);
if(sent && attempts) return '并发重试 '+sent+'/'+attempts;
const step=String(r.step||'');
const cut=step.indexOf(' · ');
return cut>0?step.slice(0,cut):step;
}
function jobStepHistory(j){
const r=jobResult(j)||{};
const steps=Array.isArray(r.logs)&&r.logs.length?r.logs.map(s=>String(s||'').trim()).filter(Boolean):(Array.isArray(r.steps)?r.steps.map(s=>String(s||'').trim()).filter(Boolean):[]);
if(!steps.length){
const cur=String(r.step||'').trim();
return cur?cur:'';
}
return steps.map((s,i)=>(i+1)+'. '+s).join('\n');
}
function jobSummaryLines(j){
const lines=[];
if(jobIsRotate(j)) lines.push(j.type==='login'?'轮换后重新登录':'重置设备身份并重新登录');
const rec=jobReceipt(j);
const route=extendRoute(j);
if(route) lines.push('加时 '+(route.at+1)+'/'+route.steps.length+' · '+extendRouteText(route));
else if(rec.sku) lines.push((rec.package?rec.package+' / ':'')+rec.sku);
else if(j.type==='redeem_gift'){
const code=String((jobPayload(j)||{}).code||(jobResult(j)||{}).code||'');
if(code) lines.push(code);
}
const progress=jobProgressLine(j);
if(progress && (j.type==='login'||j.type==='redeem_gift'||j.status==='running'||j.status==='queued'||j.status==='failed'||j.status==='cancelled')) lines.push(progress);
const fails=jobFailText(j);
if(fails) fails.split('\n').forEach(s=>{ if(s) lines.push(s); });
else if(j.error) lines.push(j.error);
else if(j.challenge_type) lines.push('验证: '+j.challenge_type+(j.challenge_hint?(' / '+j.challenge_hint):''));
return lines;
}
function jobSummaryHTML(j){
const lines=jobSummaryLines(j);
if(!lines.length) return '-';
const shown=lines.slice(0,3);
if(lines.length>3) shown[2]=shown[2]+' …+'+(lines.length-3);
return ''+shown.map(s=>'
'+esc(s)+'
').join('')+'
';
}
function jobCell(label, text){
text=String(text||'').trim();
if(!text) return '';
return ''+esc(label)+' '+esc(text)+'
';
}
function jobEgressText(j){
const p=jobPayload(j)||{};
const r=jobResult(j)||{};
const proxy=String(p.proxy||r.proxy||'').trim();
const cc=String(p.egress_country||r.egress_country||'').toUpperCase();
const ip=String(p.egress_ip||r.egress_ip||'').trim();
const bits=[];
if(proxy) bits.push(proxy);
if(ip||cc) bits.push([ip, cc].filter(Boolean).join(' · '));
return bits.join(' · ');
}
function bindEgressText(j){
const p=jobPayload(j)||{};
const r=jobResult(j)||{};
const proxy=String(p.proxy||'');
const cc=String(p.egress_country||r.egress_country||'').toUpperCase();
const ip=String(p.egress_ip||r.egress_ip||'');
const place=[cc, p.egress_region||r.egress_region, p.egress_city||r.egress_city].filter(Boolean).join(' / ');
const pool=proxy.indexOf('pool:')===0?proxy.slice(5).split('|')[0]:'';
const picked=((proxy.match(/cc=([A-Za-z]{2})/)||[])[1]||'').toUpperCase();
return [pool?('代理池 '+pool):'', picked?('选择 '+picked):'', ip, place].filter(Boolean).join(' · ');
}
function jobDetail(j){
const bits=[];
if(j.error) bits.push(j.error);
if(j.challenge_type) bits.push('验证: '+j.challenge_type+(j.challenge_hint?(' / '+j.challenge_hint):''));
try{
const r=jobResult(j);
if(r && r.step) bits.push('进度: '+r.step);
}catch(e){}
const payload=prettyJSON(j.payload);
const result=prettyJSON(j.result);
if(payload) bits.push('请求:\n'+payload);
if(result) bits.push('结果:\n'+result);
return bits.join('\n\n') || '-';
}
function showJobReceipt(id){
const j=(cache.jobs||[]).find(x=>Number(x.id)===Number(id));
if(!j) return fail('无法打开详情','找不到任务 #'+id);
let timer=0;
const draw=(cur)=>{
const rec=jobReceipt(cur);
const row=(k,v)=> v ? ''+esc(k)+''+esc(v)+'' : '';
const prog=jobProgress(cur);
const html=''+
row('任务','#'+cur.id+' · '+jobTypeLabel(cur)+' · '+statusName(cur.status))+
row('账号', rec.email||('- #'+cur.account_id))+
row('代理出口', jobEgressText(cur))+
row('商品', [rec.package, rec.sku, rec.product_type].filter(Boolean).join(' / '))+
(extendRoute(cur)?'加时路径'+extendRouteHTML(extendRoute(cur))+'':'')+
row('支付卡', rec.card)+
(cur.type==='bind_instrument'?row('出口', bindEgressText(cur)):'')+
row('重试次数', rec.replay)+
((cur.type==='purchase'||cur.type==='sub_renew'||cur.type==='extend'||cur.type==='redeem_gift')?row('并发请求数', String((jobPayload(cur)||{}).replay_concurrency ?? (jobResult(cur)||{}).concurrency ?? '')):'')+
((cur.type==='purchase'||cur.type==='sub_renew'||cur.type==='extend')?'出口代理组'+esc(String((jobPayload(cur)||{}).proxy_group||'').trim()||'无')+'':'')+
row('成功出口', (jobResult(cur)||{}).replay_proxy||'')+
row('基础方案', rec.base_plan_id)+
row('计费周期', rec.billing_period)+
row('订单号', rec.order_id)+
row('Purchase token', rec.purchase_token)+
row('验证', cur.challenge_type ? (cur.challenge_type+(cur.challenge_hint?(' / '+cur.challenge_hint):'')) : '')+
(rec.error?'错误'+esc(rec.error)+'':'')+
row('重试进度', jobProgressLine(cur))+
(jobStepHistory(cur)?'过程'+esc(jobStepHistory(cur))+'':'')+
(jobFailText(cur)?'失败'+esc(jobFailText(cur))+'':'')+
'
'+
'请求
'+(esc(prettyJSON(cur.payload))||'-')+'
'+
'结果
'+(esc(prettyJSON(cur.result))||'-')+'
';
const body=document.querySelector('#jobReceiptBody');
if(body) body.innerHTML=html;
};
const root=$('#modalRoot');
root.innerHTML='';
draw(j);
const close=()=>{ if(timer) clearInterval(timer); document.removeEventListener('keydown', onEsc); root.innerHTML=''; };
const onEsc=e=>{ if(e.key==='Escape'){ e.preventDefault(); close(); } };
document.addEventListener('keydown', onEsc);
bindMaskClose(root, close);
$('#mOk').onclick=close;
if(j.status==='running'||j.status==='queued'||j.status==='challenge_required'){
timer=setInterval(async()=>{
if(!document.querySelector('#jobReceiptBody')){ clearInterval(timer); return; }
const fresh=await api('/api/jobs/'+id);
if(!fresh || fresh.error) return;
const i=(cache.jobs||[]).findIndex(x=>Number(x.id)===Number(id));
if(i>=0) cache.jobs[i]=Object.assign({}, cache.jobs[i], fresh);
draw(i>=0?cache.jobs[i]:fresh);
if(fresh.status && fresh.status!=='running' && fresh.status!=='queued' && fresh.status!=='challenge_required') clearInterval(timer);
}, 2000);
}
}
function jobClick(ev,id){
if(ev.target.closest('button,input,a,select')) return;
const j=(cache.jobs||[]).find(x=>Number(x.id)===Number(id));
if(j && j.status==='challenge_required' && (j.challenge_type==='otp' || j.challenge_type==='3ds' || j.challenge_type==='2sv' || j.challenge_type==='prompt' || !j.challenge_type)){
solve(id);
return;
}
showJobReceipt(id);
}
function emails(){ const m={}; (cache.accounts||[]).forEach(a=>{ if(a) m[a.id]=a.email; }); return m; }
function accClick(ev,id){
if(ev.target.closest('button,input,a')) return;
showAccount(id);
}
function purchaseMeta(p){
if(!p) return null;
let meta=p.meta||p.meta_json;
if(!meta) return null;
if(typeof meta==='string'){ try{ meta=JSON.parse(meta); }catch(e){ return null; } }
return meta;
}
function currentPackageSubs(subs){
const best=new Map();
for(const s of subs||[]){
if(!s) continue;
const key=(s.account_id||0)+'|'+String(s.package||'');
const prev=best.get(key);
const time=Number(s.purchase_time||0);
const prevTime=prev?Number(prev.purchase_time||0):0;
const live=subCurrent(s), prevLive=prev?subCurrent(prev):false;
if(!prev || (live && !prevLive) || (live===prevLive && (time>prevTime || (time===prevTime && Number(s.id||0)>Number(prev.id||0))))) best.set(key,s);
}
return [...best.values()];
}
function mergeDeferredSubs(subs){
subs=(subs||[]).filter(Boolean);
const used=new Set();
const merged=[];
const chains=[];
for(const p of (cache.purchases||[])){
const meta=purchaseMeta(p)||{};
const fromSku=p.upgrade_from_sku||meta.upgrade_from_sku;
const upgradeType=p.upgrade_type||meta.upgrade_type;
const repl=Number(p.replacement_mode||meta.replacement_mode||0);
if(!fromSku || upgradeType==='new') continue;
if(repl!==5) continue;
const time=purchaseTimeUnix(p);
chains.push({p, fromSku, toSku:p.sku||'', repl, time});
}
chains.sort((a,b)=>a.time-b.time);
const byKey=new Map();
for(const step of chains){
const key=step.p.account_id+'|'+step.p.package;
let chain=byKey.get(key);
if(!chain){
chain={steps:[], from:null, to:null};
byKey.set(key, chain);
}
const from=subs.find(s=>s.account_id===step.p.account_id && s.package===step.p.package && s.sku===step.fromSku);
const to=subs.find(s=>s.account_id===step.p.account_id && s.package===step.p.package && s.sku===step.toSku)
|| subs.find(s=>s.account_id===step.p.account_id && s.package===step.p.package && s.purchase_token && s.purchase_token===purchaseTokenOf(step.p) && s.sku!==step.fromSku);
if(!from || !to || from===to) continue;
if(!chain.from) chain.from=from;
chain.to=to;
chain.steps.push(step);
chain.repl=step.repl;
used.add(from); used.add(to);
}
for(const chain of byKey.values()){
if(!chain.from || !chain.to || !chain.steps.length) continue;
const skus=[chain.from.sku];
chain.steps.forEach(s=>{ if(s.toSku && skus[skus.length-1]!==s.toSku) skus.push(s.toSku); });
merged.push(Object.assign({}, chain.to, {
sku_label: skus.join(' → '),
status_label: '到期后切换',
acknowledged: true,
purchase_time: chain.from.purchase_time||chain.to.purchase_time,
purchase_token: chain.to.purchase_token||chain.from.purchase_token,
order_id: chain.to.order_id||chain.from.order_id,
auto_renewing: !!(chain.to.auto_renewing||chain.from.auto_renewing)
}));
}
return subs.filter(s=>!used.has(s)).concat(merged);
}
function subCurrent(x){
if(!x) return false;
if(x.status==='cancelled' || x.status==='replaced') return false;
return x.status==='active' || !!x.expiry;
}
function runningUpgrade(aid){
return (cache.jobs||[]).filter(j=>j && Number(j.account_id)===Number(aid) && (j.type==='sub_renew'||j.type==='extend'||j.type==='purchase') && (j.status==='running'||j.status==='queued')).sort((a,b)=>b.id-a.id)[0];
}
function upgradeButton(aid, tok, pkg, sku){
const job=runningUpgrade(aid);
if(!job) return '';
const text=jobProgressLine(job)||jobStepText(job)||statusName(job.status);
return '';
}
function subAction(aid,x){
const pkg=esc(x.package||''), sku=esc(x.sku||''), tok=esc(x.purchase_token||'');
const up=upgradeButton(aid, tok, pkg, sku);
if(x.status==='cancelled' || x.auto_renewing===false) return up+'';
return up+'';
}