function toggleJobStatus(status, on){
if(!Array.isArray(filters.jobStatus)) filters.jobStatus=[];
filters.jobStatus=on?filters.jobStatus.concat(status):filters.jobStatus.filter(s=>s!==status);
pageNo.jobs=1;
render(false);
}
function jobActions(j){
const open=j.status==='queued'||j.status==='running'||j.status==='challenge_required';
return (open?'':'')+
(j.status==='failed'||j.status==='cancelled'?'':'')+
(j.status==='challenge_required'?'':'');
}
function jobIsExtend(j){
const p=jobPayload(j);
return !!(p && (p.extend_path || p.extend_chain || Number(p.extend_step)>0));
}
function jobIsRotate(j){
const p=jobPayload(j);
return !!(p && (p.rotate_device || p.via==='after_rotate'));
}
function jobTypeLabel(j){
if(jobIsRotate(j)) return '轮换设备';
if(jobIsExtend(j)) return '加时';
return typeName(j.type);
}
function viewJobs(){
const types=['','login','rotate','keepalive','bind_instrument','redeem_gift','sku_details','purchase','sub_renew','extend','sub_refresh','sub_cancel','sub_ack'];
const statuses=['queued','running','challenge_required','succeeded','failed','cancelled'];
if(!Array.isArray(filters.jobStatus)) filters.jobStatus=filters.jobStatus?[filters.jobStatus]:[];
const all=(cache.jobs||[]).filter(j=>{
if(!j) return false;
if(filters.jobType==='extend' && !jobIsExtend(j)) return false;
if(filters.jobType==='rotate' && !jobIsRotate(j)) return false;
if((filters.jobType==='keepalive' || filters.jobType==='login') && jobIsRotate(j)) return false;
if(filters.jobType==='sub_renew' && jobIsExtend(j)) return false;
if(filters.jobType && filters.jobType!=='extend' && filters.jobType!=='rotate' && j.type!==filters.jobType) return false;
if(!filters.jobStatus.includes(j.status)) return false;
return matchQ([j.email,j.type,j.status,j.error,String(j.id),String(j.account_id),jobSummary(j),jobTypeLabel(j)]);
});
const info=paginate(all,'jobs');
const typeLabel=t=>t==='extend'?'加时':(t==='rotate'?'轮换设备':typeName(t));
const chips='
'+types.map(t=>'').join('')+'
';
const statusPick=''+statuses.map(s=>'').join('')+'
';
const rows=info.slice.map(j=>{
const open=j.status==='queued'||j.status==='running'||j.status==='challenge_required';
return ''+
'| #'+j.id+' | '+
''+esc(jobTypeLabel(j))+' | '+
''+esc(j.email||('#'+j.account_id))+' | '+
''+pill(j.status)+' | '+
'创建 '+fmtTime(j.created_at)+' 开始 '+fmtTime(j.started_at)+' 完成 '+fmtTime(j.finished_at)+' | '+
''+jobSummaryHTML(j)+' | '+
''+jobActions(j)+' |
';
}).join('');
$('#app').innerHTML=
''+chips+''+all.length+' / '+(cache.jobs||[]).length+''+statusPick+'
'+
'| ID | 类型 | 账号 | 状态 | 创建 / 开始 / 完成 | 摘要 | |
'+
(rows||'| 没有任务。 |
')+
'
'+pager('jobs', info);
}
async function cancelJob(id){
const ok=await modal({title:'取消任务', message:'取消任务 #'+id+'?', okText:'取消任务', cancelText:'返回'});
if(!ok) return;
const j=await api('/api/jobs/'+id+'/cancel',{method:'POST',body:'{}'});
if(j.error) return fail('无法取消', j.error);
notice('已取消', '#'+id);
render();
}
function outsideVaultCard(job){
const p=jobPayload(job);
const order=String((p.old_order_id||p.order_id)||'');
if(!order) return null;
const rec=(cache.purchases||[]).find(x=>x && Number(x.account_id)===Number(job.account_id) && String(x.order_id||'')===order);
let last4=String((rec&&rec.card_last4)||'');
if(!last4){
const acc=(cache.accounts||[]).find(a=>a && Number(a.id)===Number(job.account_id));
const card=((acc&&acc.cards)||[]).find(c=>c && c.last4);
last4=String((card&&card.last4)||'');
}
if(!last4) return null;
const card=(cache.cards||[]).find(c=>c && String(c.last4||'')===last4);
if(!card) return last4;
if(card.no_replay_concurrency) return last4+'(本卡已设置禁止并发)';
return null;
}
async function retryProxyDialog(outsideLast4){
let groups=[];
try{
const st=await api('/api/settings');
groups=(st.proxy_groups||[]).filter(g=>g && (g.proxies||[]).length);
}catch(e){}
return new Promise(resolve=>{
const root=$('#modalRoot');
const rows=groups.map((g,i)=>{
const n=(g.proxies||[]).length;
return '';
}).join('');
const outside=!!outsideLast4;
root.innerHTML='';
const finish=v=>{ root.innerHTML=''; resolve(v); };
let unlocked=!outside;
const unlock=$('#unlockConc');
if(unlock) unlock.onclick=async()=>{
const ok=await modal({title:'解除并发锁定', message:'这张卡不在本系统卡库,并发重试可能打出多笔订单。确定要解除锁定并自行设置并发吗?', okText:'解除锁定', cancelText:'保持锁定', stack:true});
if(!ok) return;
unlocked=true;
const input=$('#retryConc');
if(input) input.disabled=false;
unlock.disabled=true;
unlock.textContent='已解除';
};
bindMaskClose(root, ()=>finish(null));
$('#mCancel').onclick=()=>finish(null);
$('#mOk').onclick=()=>{
const radio=root.querySelector('input[name=retryProxy]:checked');
let egress='';
if(radio){
const row=radio.closest('.mode-pick');
egress=((row.querySelector('.retry-egress')||{}).value||'').trim();
}
const pauseOn=!!($('#retryPause')&&$('#retryPause').checked);
finish({
concurrency:(outside&&!unlocked)?1:Math.max(1, Number($('#retryConc').value||5)),
unlock_concurrency:outside&&unlocked,
attempts:Math.max(1, Number($('#retryN').value||3000)),
group:radio?radio.value:'',
egress:egress,
immediate:!!($('#retryNow')&&$('#retryNow').checked),
rotate:!!($('#retryRotate')&&$('#retryRotate').checked),
rotate_every:pauseOn?Math.max(0, Number($('#retryPauseN').value||0)):0
});
};
document.addEventListener('keydown', function onKey(e){
if(e.key==='Escape'){ document.removeEventListener('keydown', onKey); finish(null); }
});
});
}
async function redeemRetryDialog(){
return new Promise(resolve=>{
const root=$('#modalRoot');
root.innerHTML='';
const finish=v=>{ root.innerHTML=''; resolve(v); };
bindMaskClose(root, ()=>finish(null));
$('#mCancel').onclick=()=>finish(null);
$('#mOk').onclick=()=>finish({
concurrency:Math.max(1, Number($('#retryConc').value||30)),
attempts:Math.max(1, Number($('#retryN').value||3000)),
immediate:!!($('#retryNow')&&$('#retryNow').checked)
});
});
}
async function retryJob(id, type){
let body={};
if(type==='purchase' || type==='sub_renew' || type==='extend' || type==='redeem_gift'){
const job=(cache.jobs||[]).find(j=>j && Number(j.id)===Number(id));
const outside=type==='redeem_gift'?null:(job?outsideVaultCard(job):null);
const picked=type==='redeem_gift'?await redeemRetryDialog():await retryProxyDialog(outside);
if(!picked) return;
body={concurrency:picked.concurrency, attempts:picked.attempts, proxy_group:picked.group, proxy_egress:picked.egress, immediate:!!picked.immediate, rotate_device:!!picked.rotate, rotate_every:Number(picked.rotate_every||0), unlock_concurrency:!!picked.unlock_concurrency};
}else{
const ok=await modal({title:'重试任务', message:'重新入队任务 #'+id+'?', okText:'重试', cancelText:'取消'});
if(!ok) return;
}
const j=await api('/api/jobs/'+id+'/retry',{method:'POST',body:JSON.stringify(body)});
if(j.error) return fail('无法重试', j.error);
notice('已重新入队', '#'+(j.job_id||id));
refreshList();
}