// ==UserScript== // @name GenGen-RMJ-DET // @namespace http://tampermonkey.net/ // @version 4.6.7 // @author GenGen 队 // @match https://www.luogu.com.cn/record/cf/* // @match https://www.luogu.com.cn/record/at/* // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect codeforces.com // @connect www.luogu.com.cn // @connect atcoder.jp // @run-at document-start // @description GenGen RMJ 提交记录详情 // @license MIT // ==/UserScript== (function() { const _gmGet = GM_getValue; const _gmReq = GM_xmlhttpRequest; const VALID = [ /^https:\/\/www\.luogu\.com\.cn\/record\/cf\/[^/]+\/[^/?#]+/, /^https:\/\/www\.luogu\.com\.cn\/record\/at\/[^/]+\/[^/?#]+/, ]; if (!VALID.some(r => r.test(location.href))) return; const m = location.pathname.match(/\/record\/(cf|at)\/([^/]+)\/([^/?#]+)/); if (!m) return; const [, plat, cid, sid] = m; const isCF = plat === 'cf'; const isAT = plat === 'at'; const handle = _gmGet('cf-handle', '') || _gmGet('rmj-cf-handle', '') || ''; const code = _gmGet(`{${cid},${sid}}`, ''); const titleTimer = setInterval(() => { document.title = 'GenGen RMJ 加载中'; }, 100); const mask = document.createElement('div'); mask.id = 'rmj-mask'; mask.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgb(239,239,239);z-index:2147483647;'; (document.documentElement || document.body || document).appendChild(mask); const CF2LG = { OK:12, WRONG_ANSWER:6, TIME_LIMIT_EXCEEDED:5, MEMORY_LIMIT_EXCEEDED:4, OUTPUT_LIMIT_EXCEEDED:3, RUNTIME_ERROR:7, COMPILATION_ERROR:2, TESTING:1, WAITING:0 }; const FINAL_CF = new Set(['OK','WRONG_ANSWER','COMPILATION_ERROR','RUNTIME_ERROR', 'TIME_LIMIT_EXCEEDED','MEMORY_LIMIT_EXCEEDED','OUTPUT_LIMIT_EXCEEDED']); const TC = 'Luogu\\DataClass\\Record\\JudgeResult\\TestCaseJudgeResult'; const VERB_DESC = { 'OK': 'ok accepted', 'WRONG_ANSWER': 'wrong answer', 'TIME_LIMIT_EXCEEDED': 'time limit exceeded', 'MEMORY_LIMIT_EXCEEDED': 'memory limit exceeded', 'OUTPUT_LIMIT_EXCEEDED': 'output limit exceeded', 'RUNTIME_ERROR': 'runtime error', 'COMPILATION_ERROR': 'compilation error', 'TESTING': 'judging', 'WAITING': 'waiting' }; const gmJson = (url) => new Promise((ok, no) => { _gmReq({ method:'GET', url, timeout:15000, anonymous:true, onload: r => { try { ok(JSON.parse(r.responseText)); } catch (e) { no(e); } }, onerror: () => no(new Error('net')), ontimeout: () => no(new Error('timeout')) }); }); const gmText = (url) => new Promise((ok, no) => { _gmReq({ method:'GET', url, timeout:15000, anonymous:true, onload: r => r.status === 200 ? ok(r.responseText) : no(new Error('status ' + r.status)), onerror: () => no(new Error('net')), ontimeout: () => no(new Error('timeout')) }); }); const fetchTemplate = () => new Promise((ok, no) => { _gmReq({ method:'GET', url:'https://www.luogu.com.cn/record/290510067', timeout:15000, onload: r => r.status === 200 ? ok(r.responseText) : no(new Error('tpl')), onerror: () => no(new Error('tpl net')), ontimeout: () => no(new Error('tpl timeout')) }); }); async function findCF() { let url = `https://codeforces.com/api/contest.status?contestId=${encodeURIComponent(cid)}&from=1&count=100`; if (handle) url += `&handle=${encodeURIComponent(handle)}`; const d = await gmJson(url); if (d.status !== 'OK') throw new Error('CF ' + d.status); return (d.result || []).find(x => String(x.id) === sid) || null; } function parseATStatus(text) { if (!text) return 'TESTING'; text = text.trim(); if (text === 'AC') return 'OK'; if (text === 'WA') return 'WRONG_ANSWER'; if (text === 'TLE' || text.includes('TLE')) return 'TIME_LIMIT_EXCEEDED'; if (text === 'MLE') return 'MEMORY_LIMIT_EXCEEDED'; if (text === 'OLE') return 'OUTPUT_LIMIT_EXCEEDED'; if (text === 'RE') return 'RUNTIME_ERROR'; if (text === 'CE') return 'COMPILATION_ERROR'; if (text === 'WJ') return 'WAITING'; if (text.includes('WA')) return 'WRONG_ANSWER'; return 'TESTING'; } async function findAT() { const url = `https://atcoder.jp/contests/${cid}/submissions/${sid}`; const html = await gmText(url); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const metaTable = doc.querySelector('table.table-bordered'); const metaRows = metaTable ? metaTable.querySelectorAll('tr') : []; const meta = {}; metaRows.forEach(row => { const th = row.querySelector('th'); const td = row.querySelector('td'); if (th && td) meta[th.textContent.trim()] = td.textContent.trim(); }); const codeEl = doc.querySelector('#submission-code'); const sourceCode = codeEl ? codeEl.textContent : ''; const tables = doc.querySelectorAll('table.table-bordered'); const cases = []; let passedCount = 0; let caseTable = null; for (const t of tables) { const header = t.querySelector('thead th, tr th'); if (header && (header.textContent.includes('Case Name') || header.textContent.includes('Status'))) { caseTable = t; break; } } if (caseTable) { caseTable.querySelectorAll('tbody tr').forEach((row, index) => { const tds = row.querySelectorAll('td'); if (tds.length >= 4) { const statusText = tds[1].textContent.trim(); const timeMatch = tds[2].textContent.trim().match(/(\d+)/); const memMatch = tds[3].textContent.trim().match(/(\d+)/); const verdict = parseATStatus(statusText); if (verdict === 'OK') passedCount++; cases.push({ id: `${index + 1}`, status: verdict, time: timeMatch ? parseInt(timeMatch[1]) : null, memKB: memMatch ? parseInt(memMatch[1]) : null }); } }); } let overallVerdict = parseATStatus(meta['Status'] || ''); if (overallVerdict === 'OK' && passedCount < cases.length && cases.length > 0) overallVerdict = 'TESTING'; const timeMatch = (meta['Exec Time'] || '').match(/(\d+)/); const memMatch = (meta['Memory'] || '').match(/(\d+)/); const taskText = meta['Task'] || ''; let taskIndex = 'a'; const taskLink = metaTable ? metaTable.querySelector('a[href*="/tasks/"]') : null; if (taskLink) { const linkMatch = (taskLink.getAttribute('href') || '').match(/\/tasks\/[^_]+_(.+)$/i); if (linkMatch) taskIndex = linkMatch[1].toLowerCase(); } else { const textMatch = taskText.match(/^([A-Za-z0-9]+)/); if (textMatch) taskIndex = textMatch[1].toLowerCase(); } let submitTime = 0; const timeStr = meta['Submission Time'] || ''; if (timeStr) { const tm = timeStr.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})([+-]\d{2})(\d{2})$/); submitTime = tm ? Math.floor(new Date(`${tm[1]}T${tm[2]}${tm[3]}:${tm[4]}`).getTime() / 1000) : (Math.floor(new Date(timeStr).getTime() / 1000) || 0); } return { isAT: true, verdict: overallVerdict, passedTestCount: passedCount, timeConsumedMillis: timeMatch ? parseInt(timeMatch[1]) : 0, memoryConsumedBytes: memMatch ? parseInt(memMatch[1]) * 1024 : 0, problem: { name: taskText, index: taskIndex }, sourceCode, cases, creationTimeSeconds: submitTime }; } function buildCases(sub, time, memKB) { if (sub.isAT) { return sub.cases.map(c => ({ id: c.id, status: CF2LG[c.status] ?? 0, description: VERB_DESC[c.status] || String(c.status).toLowerCase().replace(/_/g, ' '), score: null, signal: 0, exitCode: 0, subtaskID: 0, __CLASS_NAME: TC, time: c.time ?? null, memory: c.memKB ?? null })); } const lgCode = CF2LG[sub.verdict] ?? 0; const isAC = sub.verdict === 'OK'; const isCE = sub.verdict === 'COMPILATION_ERROR'; const passed = sub.passedTestCount ?? 0; if (isCE) return []; let idNum, status; if (isAC) { idNum = passed > 0 ? passed : 1; status = 12; } else { idNum = passed + 1; status = (sub.verdict === 'TESTING') ? 1 : lgCode; } return [{ id: `${idNum}`, status, description: VERB_DESC[sub.verdict] || String(sub.verdict).toLowerCase().replace(/_/g, ' '), score: null, signal: 0, exitCode: 0, subtaskID: 0, __CLASS_NAME: TC, time: time ?? null, memory: memKB ?? null }]; } function buildRecordSubset(sub, recId) { const lgCode = CF2LG[sub.verdict] ?? 0; const isCE = sub.verdict === 'COMPILATION_ERROR'; const isFinal = FINAL_CF.has(sub.verdict); const time = isFinal ? (sub.timeConsumedMillis ?? 0) : 0; const memKB = isFinal ? Math.round((sub.memoryConsumedBytes ?? 0) / 1024) : 0; const cases = buildCases(sub, time, memKB); const tcMap = {}; cases.forEach(c => { tcMap[c.id] = c; }); return JSON.parse(JSON.stringify({ detail: { __CLASS_NAME:'Luogu\\DataClass\\Record\\RecordDetail', compileResult:{ __CLASS_NAME:'Luogu\\DataClass\\Record\\JudgeResult\\CompileResult', message:null, opt2:false, success:!isCE }, judgeResult:{ __CLASS_NAME:'Luogu\\DataClass\\Record\\JudgeResult\\JudgeResult', finishedCaseCount:cases.length, memory:memKB, score: null, status:lgCode, subtasks:[{ __CLASS_NAME:'Luogu\\DataClass\\Record\\JudgeResult\\SubtaskJudgeResult', id:0, judger:'', memory:memKB, score: null, status:lgCode, testCases:tcMap, time }], time }, version:400 }, enableO2:true, id: Number(recId), memory:memKB, status:lgCode, time, score: null, })); } const buildStatusPush = (sub, id) => ({ _channel:'record.track', _channel_param:String(id), _ws_type:'server_broadcast', record:buildRecordSubset(sub, id), type:'status_push' }); const PAGE_HOOK = `(function(){var PORT=null,latestPush=null;class FakePort extends EventTarget{constructor(){super();this._om=null;}get onmessage(){return this._om;}set onmessage(fn){this._om=fn;if(latestPush&&typeof this._om==='function'){setTimeout(()=>{try{this._om({type:'message',data:JSON.stringify(latestPush)+'\\n'});}catch(e){}},500);}}start(){}close(){}postMessage(msg){let p=null;try{if(typeof msg==='string')p=JSON.parse(msg);else if(msg&&msg.type==='join_channel')p=msg;else if(msg&&msg.func==='send'&&Array.isArray(msg.parameters)){for(let item of msg.parameters){if(typeof item==='string'){p=JSON.parse(item);break;}}}}catch(e){}if(p&&latestPush){var jr={_channel:'record.track',_channel_param:p.channel_param||latestPush._channel_param,_ws_type:'join_result',client_number:1,result:'success',type:'result',welcome_message:{detail:latestPush.record.detail,memory:null,record:latestPush.record,score:latestPush.record.score||null,status:latestPush.record.status,time:null,type:'status_push'}};setTimeout(()=>this._emit({type:'message',data:JSON.stringify(jr)+'\\n'}),200);}}_emit(data){var ev=new MessageEvent('message',{data:data});this.dispatchEvent(ev);if(typeof this._om==='function')try{this._om(ev);}catch(err){}}}function FakeSW(){this.port=PORT=new FakePort();}if(window.SharedWorker){var OSW=window.SharedWorker;window.SharedWorker=new Proxy(OSW,{construct:function(){return new FakeSW();}});}window.addEventListener('message',function(e){if(e.data&&e.data.__rmj_ws_push__){latestPush=e.data.payload;if(PORT&&typeof PORT._om==='function'){PORT._emit({type:'message',data:JSON.stringify(latestPush)+'\\n'});}}});})();`; function buildHtml(tpl, sub) { tpl = tpl.replace(/
]*>/i, m => m + '`); } (async () => { if (window.__RMJ_MAIN_RAN__) return; window.__RMJ_MAIN_RAN__ = true; // ========== 缓存写入核心函数 ========== const updateCache = (sub) => { if (!sub) return; if (isCF) { const CACHE_KEY_CF = 'cf-submissions-cache'; let cache = []; try { cache = JSON.parse(GM_getValue(CACHE_KEY_CF, '[]')); if (!Array.isArray(cache)) cache = []; } catch { cache = []; } const item = { id: String(sid), taskDisplay: `CF${cid}${sub.problem.index} - ${sub.problem.name}`, status: sub.verdict || 'Waiting', timestamp: (sub.creationTimeSeconds || 0) * 1000, submitUrl: `https://www.luogu.com.cn/record/cf/${cid}/${sid}`, luoguUrl: `https://www.luogu.com.cn/problem/CF${cid}${sub.problem.index}`, passedTestCount: sub.passedTestCount }; const idx = cache.findIndex(c => c.id === String(sid)); if (idx !== -1) { cache[idx] = { ...cache[idx], ...item }; } else { cache.unshift(item); } cache.sort((a, b) => Number(b.id) - Number(a.id)); GM_setValue(CACHE_KEY_CF, JSON.stringify(cache)); GM_setValue('cf-last-update', Date.now()); } else if (isAT) { const CACHE_KEY_AT = 'at-submissions-cache'; let cache = []; try { cache = JSON.parse(GM_getValue(CACHE_KEY_AT, '[]')); if (!Array.isArray(cache)) cache = []; } catch { cache = []; } const AT_VERDICT_MAP = { 'OK': 'AC', 'WRONG_ANSWER': 'WA', 'TIME_LIMIT_EXCEEDED': 'TLE', 'MEMORY_LIMIT_EXCEEDED': 'MLE', 'OUTPUT_LIMIT_EXCEEDED': 'OLE', 'RUNTIME_ERROR': 'RE', 'COMPILATION_ERROR': 'CE', 'WAITING': 'WJ', 'TESTING': 'WJ' }; const atStatus = AT_VERDICT_MAP[sub.verdict] || 'WJ'; const FINAL_AT = new Set(['AC', 'WA', 'CE', 'RE', 'TLE', 'MLE', 'OLE', 'IE']); const taskKey = `${cid}_${sub.problem.index}`; const item = { id: String(sid), taskKey: taskKey, taskDisplay: `${taskKey} - ${sub.problem.index.toUpperCase()} - ${sub.problem.name}`, status: atStatus, submitUrl: `https://atcoder.jp/contests/${cid}/submissions/${sid}`, luoguUrl: `https://www.luogu.com.cn/problem/AT_${taskKey}`, timestamp: (sub.creationTimeSeconds || 0) * 1000, isFinal: FINAL_AT.has(atStatus) }; const idx = cache.findIndex(c => c.id === String(sid)); if (idx !== -1) { cache[idx] = { ...cache[idx], ...item }; } else { cache.unshift(item); } cache.sort((a, b) => Number(b.id) - Number(a.id)); GM_setValue(CACHE_KEY_AT, JSON.stringify(cache)); GM_setValue('at-last-update', Date.now()); } }; // ========================================== const cfDomainPath = Number(cid) > 100000 ? 'gym' : 'contest'; const origUrl = isCF ? `https://codeforces.com/${cfDomainPath}/${cid}/submission/${sid}` : `https://atcoder.jp/contests/${cid}/submissions/${sid}`; let sub = null; try { sub = isCF ? await findCF() : (isAT ? await findAT() : null); } catch (e) { if (isAT) { clearInterval(titleTimer); location.replace(origUrl); return; } } if (isCF && sub === null) { clearInterval(titleTimer); location.replace(origUrl); return; } // ========== 初次抓取成功直接写入缓存 ========== if (sub) updateCache(sub); // ===================================================== const tpl = await fetchTemplate().catch(() => null); if (!tpl) return; clearInterval(titleTimer); const parser = new DOMParser(); const doc = parser.parseFromString(buildHtml(tpl, sub), 'text/html'); document.replaceChild(doc.documentElement, document.documentElement); document.querySelectorAll('script').forEach(oldScript => { const newScript = document.createElement('script'); Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value)); newScript.textContent = oldScript.textContent; oldScript.parentNode.replaceChild(newScript, oldScript); }); let currentSub = sub; // AtCoder 终态无详情的等待轮询状态(供500ms UI定时器读取) let atWaitPollingId = null; setInterval(() => { const m = document.getElementById('rmj-mask'); if (m && (document.querySelector('#app') || document.querySelector('.lfe-body'))) m.remove(); const hasCode = (isAT && currentSub && currentSub.sourceCode) || code; if (!hasCode) { const vals = document.querySelectorAll('.value.lfe-caption'); if (vals.length >= 2 && vals[1].textContent !== '-') vals[1].textContent = '-'; const ce = document.querySelector('pre code'); if (ce) { const pre = ce.closest('pre'); if (pre) pre.outerHTML = `