// ==UserScript== // @name VJudge Duel++ // @match https://duel.gengen.qzz.io/* // @match https://www.luogu.com.cn/record/list* // @match https://www.luogu.com.cn/record/* // @icon https://cdn.luogu.com.cn/upload/image_hosting/lb8qcsrx.png // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @connect duel.gengen.qzz.io // @run-at document-idle // @version 1.0 // @license All Rights Reserved // @description GenGen RMJ AND VJudge Duel 增强插件 // ==/UserScript== (function(){'use strict'; const L=console.log,h=location.host,p=location.pathname,s=location.search+location.hash; const g=k=>GM_getValue(k,null); const v=(k,d)=>{L('[VJudge Duel++] 写入GM存储:',k,d);GM_setValue(k,d)}; const d=k=>{L('[VJudge Duel++] 删除GM存储:',k);GM_deleteValue(k)}; const sent=new Set(); if(h.includes('duel.gengen.qzz.io')){ L('[VJudge Duel++] 检测到Duel对战页面'); const raw=localStorage.getItem('vjudge-duel.session'); if(raw){ try{const usr=JSON.parse(raw).username;L('[VJudge Duel++] 成功解析LocalStorage用户:',usr);v('u',usr)} catch(e){L('[VJudge Duel++] LocalStorage解析失败:',e)} }else{ const cu=g('u'); if(cu)L('[VJudge Duel++] LocalStorage为空,使用GM缓存用户:',cu); else L('[VJudge Duel++] 未找到vjudge-duel.session且无缓存'); } const rm=s.match(/room=([a-f0-9]+)/),sc=s.match(/secret=([a-f0-9]+)/); L('[VJudge Duel++] URL参数解析 - 房间号:',rm?.[1],'密钥:',!!sc?.[1]); if(rm&&sc){ v('r',{id:rm[1],s:sc[1]}); L('[VJudge Duel++] 开始持续轮询题目列表节点...'); const pt=setInterval(()=>{ const nds=document.querySelectorAll('.problem-heading a'); if(!nds.length)return; clearInterval(pt); const ps=[]; nds.forEach(nd=>{ const txt=nd.innerText.trim(),pf=txt.split(/\s+/)[0]; let pid=txt.replace(/^\S+\s*/,'').replace(/^[_\-]/,''); if(pf.toLowerCase()==='atcoder')pid=pid.toLowerCase(); else if(pf.toLowerCase()==='codeforces')pid=pid.toUpperCase(); if(pid)ps.push(pid); }); L('[VJudge Duel++] 题目列表解析完成(已过滤空值):',ps); v('p',ps); },500); } setInterval(()=>{ if(document.body.innerText.match(/比赛已封档|只读观赛/)){ L('[VJudge Duel++] 检测到比赛封档/只读观赛,清除当前房间数据'); d('u');d('r');d('p'); } },10000); } if(h.includes('www.luogu.com.cn')){ L('[VJudge Duel++] 检测到洛谷提交记录页面'); const u=g('u'),rm=g('r'),pbs=g('p'); L('[VJudge Duel++] 读取GM数据 - 用户:',u,'房间:',rm,'题目列表:',pbs); if(!u||!rm||!pbs||!pbs.length){L('[VJudge Duel++] GM数据缺失或题目列表为空,终止执行');return} const sendClaim=(np)=>{ if(sent.has(np))return; sent.add(np); L('[VJudge Duel++] 匹配成功!发送Manual Claim请求, 题号:',np); GM_xmlhttpRequest({method:'POST',url:`https://duel.gengen.qzz.io/api/rooms/${rm.id}/manual-claim?secret=${rm.s}`,headers:{'content-type':'application/json'},data:JSON.stringify({userName:u,pid:np}),onload:r=>L('[VJudge Duel++] Claim请求响应:',r.status,r.responseText),onerror:e=>L('[VJudge Duel++] Claim请求失败:',e)}); }; // === 单条记录详情页 (30s超时) === const dm=p.match(/^\/record\/(\d+)$/); if(dm){ L('[VJudge Duel++] 检测到单条记录详情页:',dm[1]); L('[VJudge Duel++] 开始30秒内轮询指定状态节点...'); const st=Date.now(); const dt=setInterval(()=>{ if(Date.now()-st>30000){clearInterval(dt);L('[VJudge Duel++] 30秒超时,未检测到终态,终止');return} const se=document.querySelector("#app > div.main-container > main > div > section.side > div > div.info-rows > div:nth-child(2) > span:nth-child(2) > span"); const pl=document.querySelector("#app > div.main-container > main > div > section.side > div > div.info-rows > div:nth-child(1) > span:nth-child(2) > span > div > a"); if(!se||!pl)return; const stx=se.innerText.trim(); if(!stx||/waiting|judging|pending|compiling|running/i.test(stx))return; if(stx!=='AC'&&stx!=='Accepted'){clearInterval(dt);L('[VJudge Duel++] 终态为:'+stx+',非AC/Accepted,终止');return} clearInterval(dt); let np=(pl.href.match(/\/problem\/(.+)$/)||[])[1]||''; const sp=pl.querySelector('.pid'); if(sp)np=sp.innerText.trim()||np; L('[VJudge Duel++] 详情页原始题目ID:',np); if(/^AT_/i.test(np))np=np.replace(/^AT_/i,'').toLowerCase(); else if(/^CF/i.test(np))np=np.replace(/^CF/i,'').toUpperCase(); L('[VJudge Duel++] 归一化后题目ID:',np,'比赛题目列表:',pbs); if(!pbs.includes(np)){L('[VJudge Duel++] 该题目不在当前比赛列表中,跳过Claim');return} sendClaim(np); },500); return; } // === 提交记录列表页 === const mj=new URLSearchParams(location.search).get('rmj'); L('[VJudge Duel++] RMJ模式参数:',mj); if(mj==='1'||mj==='2'){ // GenGen RMJ模式:无限轮询,每次取最新5条 L('[VJudge Duel++] RMJ模式开启,无限轮询最新5条.cf-card...'); setInterval(()=>{ const cards=document.querySelectorAll('.cf-card'); if(!cards.length)return; const top5=[...cards].slice(0,5); L('[VJudge Duel++] 本轮检查'+top5.length+'条.cf-card记录'); top5.forEach(card=>{ if(!card.innerHTML.includes('Accepted'))return; const al=card.querySelector('.cf-problem a'); if(!al)return; let np=(al.href.match(/\/problem\/(.+)$/)||[])[1]||''; if(mj==='2')np=np.replace(/^AT_/i,'').toLowerCase(); else if(mj==='1')np=np.replace(/^CF/i,'').toUpperCase(); if(!pbs.includes(np))return; sendClaim(np); }); },2000); }else{ // 普通洛谷模式:30s超时,仅检测最新一条.row L('[VJudge Duel++] 普通洛谷模式,30秒内轮询最新.row...'); const st=Date.now(); const ct=setInterval(()=>{ if(Date.now()-st>30000){clearInterval(ct);L('[VJudge Duel++] 30秒超时,未检测到AC,终止');return} const rw=document.querySelector('.row'); if(!rw)return; if(!rw.innerHTML.includes('Accepted'))return; const al=rw.querySelector('.problem a'); if(!al)return; clearInterval(ct); let np=(al.href.match(/\/problem\/(.+)$/)||[])[1]||''; L('[VJudge Duel++] 普通模式原始题目ID:',np); if(!pbs.includes(np)){L('[VJudge Duel++] 该题目不在当前比赛列表中,跳过Claim');return} sendClaim(np); },800); } } })();