// ==UserScript== // @name GenGen-RMJ-DET // @namespace http://tampermonkey.net/ // @version 5.1 // @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']); let baseContextData = null; 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, index) => ({ id: index, status: CF2LG[c.status] ?? 0, time: c.time ?? 0, memory: c.memKB ?? 0, score: 1, signal: 0, exitCode: 0, description: "", subtaskID: 0 })); } 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 = isAC ? (passed > 0 ? passed - 1 : 0) : passed; let status = isAC ? 12 : lgCode; if (sub.verdict === 'TESTING') status = 1; return [{ id: idNum, status, time: time ?? 0, memory: memKB ?? 0, score: 100, signal: 0, exitCode: 0, description: "", subtaskID: 0 }]; } const PAGE_HOOK = `(function(){ const originalFetch = window.fetch; window.fetch = async function(input, init) { const url = typeof input === 'string' ? input : (input && input.url ? input.url : ''); let isLentille = false; const checkHeaders = (h) => { if (!h) return; if (h['x-lentille-request'] === 'content-only') isLentille = true; else if (typeof h.get === 'function' && h.get('x-lentille-request') === 'content-only') isLentille = true; }; checkHeaders(init && init.headers); checkHeaders(input && input.headers); if (url.includes('/record/') && isLentille && window.__RMJ_LATEST_DATA__) { return new Response(JSON.stringify(window.__RMJ_LATEST_DATA__), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return originalFetch.apply(this, arguments); }; window.addEventListener('message', function(e) { if (e.data && e.data.__rmj_update_data__) { window.__RMJ_LATEST_DATA__ = e.data.payload; } }); })();`; function buildHtml(tpl, sub) { tpl = tpl.replace(/]*>/i, m => m + '`; const newJsonStr = JSON.stringify(data).replace(/${newJsonStr}${initScript}`); } const updatePageData = (sub) => { if (!baseContextData || !sub) return; const newData = JSON.parse(JSON.stringify(baseContextData)); const rec = newData?.data?.record; if (!rec) return; const lgCode = CF2LG[sub.verdict] ?? 0; const isFinal = FINAL_CF.has(sub.verdict); const time = isFinal ? (sub.timeConsumedMillis ?? 0) : 0; const memKB = isFinal ? Math.round((sub.memoryConsumedBytes ?? 0) / 1024) : 0; rec.status = lgCode; rec.time = time; rec.memory = memKB; if (rec.detail?.compileResult) rec.detail.compileResult.success = sub.verdict !== 'COMPILATION_ERROR'; // 同步修复标题与类型 if (rec.problem && sub.problem) { if (isCF) { rec.problem.pid = `CF${cid}${sub.problem.index}`; rec.problem.type = "CF"; } else if (isAT) { rec.problem.pid = `AT_${cid}_${sub.problem.index}`; rec.problem.type = "AT"; } rec.problem.name = sub.problem.name; delete rec.problem.title; } const cases = buildCases(sub, time, memKB); if (rec.detail?.judgeResult) { rec.detail.judgeResult.subtasks = [{ id: 0, score: null, status: lgCode, testCases: cases, judger: '', time, memory: memKB }]; rec.detail.judgeResult.finishedCaseCount = cases.length; rec.detail.judgeResult.status = lgCode; rec.detail.judgeResult.time = time; rec.detail.judgeResult.memory = memKB; rec.detail.judgeResult.score = null; } newData.time = Date.now() / 1000; window.postMessage({ __rmj_update_data__: true, payload: newData }, '*'); }; (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; 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 = `
本代码非 GenGen RMJ 提交,无法查看
前往原站
`; } } const card = document.querySelectorAll('.l-card')[1]; if (card && !card.querySelector('.rmj-orig-link')) { const a = document.createElement('a'); a.href = origUrl; a.target = '_blank'; a.className = 'rmj-orig-link'; a.textContent = '前往原站'; a.style.cssText = 'display:block;text-align:center;margin-top:16px;color:#3498db;text-decoration:none;'; card.appendChild(a); } if (currentSub && currentSub.verdict !== 'COMPILATION_ERROR') { const tcs = document.querySelectorAll('.test-case'); if (currentSub.isAT) { tcs.forEach((tc, idx) => { const idEl = tc.querySelector('.id'); const target = `#${idx + 1}`; if (idEl && idEl.textContent !== target) idEl.textContent = target; }); } else { const passed = currentSub.passedTestCount ?? 0; const idNum = currentSub.verdict === 'OK' ? (passed > 0 ? passed : 1) : passed + 1; const target = `#${idNum}`; tcs.forEach(tc => { const idEl = tc.querySelector('.id'); if (idEl && idEl.textContent !== target) idEl.textContent = target; }); } } if (atWaitPollingId !== null && !document.getElementById('rmj-at-wait-msg')) { const testWrap = document.querySelector('.test-case-wrap'); if (testWrap) { const waitMsg = document.createElement('div'); waitMsg.id = 'rmj-at-wait-msg'; waitMsg.textContent = '正在等待 AtCoder 完整评测完毕'; waitMsg.style.cssText = 'text-align:center;padding:12px;color:#666;font-size:14px;'; testWrap.appendChild(waitMsg); } } }, 500); if ((!isCF && !isAT) || !sub) return; if (!FINAL_CF.has(sub.verdict) && sub.verdict !== 'WAITING') { updatePageData(sub); } else if (sub.verdict === 'WAITING') { updatePageData(sub); } else { const fakeJudgingSub = sub.isAT ? { ...sub, verdict: 'TESTING', cases: sub.cases.map(c => ({...c, status: 'TESTING'})) } : { ...sub, verdict: 'TESTING', passedTestCount: Math.max(0, (sub.passedTestCount ?? 1) - 1) }; updatePageData(fakeJudgingSub); setTimeout(() => updatePageData(sub), 600); } const startAtWaitPolling = () => { if (atWaitPollingId !== null || !isAT) return; atWaitPollingId = setInterval(async () => { try { const s = await findAT(); if (s && s.cases.length > 0) { clearInterval(atWaitPollingId); atWaitPollingId = null; const msg = document.getElementById('rmj-at-wait-msg'); if (msg) msg.remove(); currentSub = s; updatePageData(s); updateCache(s); } } catch (e) {} }, 5000); }; if (isAT && sub.cases.length === 0 && sub.verdict !== 'COMPILATION_ERROR' && FINAL_CF.has(sub.verdict)) { startAtWaitPolling(); } if (!FINAL_CF.has(sub.verdict)) { let polling = false; const intervalId = setInterval(async () => { if (polling) return; polling = true; try { const s = isCF ? await findCF() : await findAT(); if (s) { currentSub = s; updatePageData(s); updateCache(s); if (FINAL_CF.has(s.verdict) && s.isAT && s.cases.length === 0 && s.verdict !== 'COMPILATION_ERROR') { startAtWaitPolling(); } if (FINAL_CF.has(s.verdict)) clearInterval(intervalId); } } catch (e) {} finally { polling = false; } }, 3000); } })(); })();