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+'
'+ '
'+ (rows||'')+ '
ID类型账号状态创建 / 开始 / 完成摘要
没有任务。
'+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(); }