// ==UserScript==
// @name GenGen-RMJ
// @icon https://cdn.luogu.com.cn/upload/image_hosting/yy27itbl.png
// @namespace https://gengen.qzz.io/
// @version 5.2.2
// @description GenGen RMJ
// @author GenGen Dev
// @run-at document-start
// @match https://challenges.cloudflare.com/cdn-cgi/challenge-platform/*
// @match https://www.luogu.com.cn/*
// @match https://atcoder.jp/contests/*/submit?RMJ=1
// @match https://atcoder.jp/contests/*/submissions/me
// @match https://codeforces.com/*
// @connect *
// @inject-into page
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
// 重定向遮罩
(() => {
const h = location.href;
if (h.includes('challenges.cloudflare.com')) return;
let flag = '';
try { flag = GM_getValue('rmj-flag-action', ''); } catch { /* ignore */ }
const ref = document.referrer || '';
if (!(ref.includes('RMJ=1') || flag === 'submit')) return;
const isStatus =
h.includes('codeforces.com/problemset/status') ||
(h.includes('atcoder.jp/contests/') && h.includes('/submissions/me'));
if (!isStatus) return;
const m = document.createElement('div');
m.id = 'rmj-redirect-mask';
m.style.cssText = 'position:fixed;inset:0;z-index:2147483647;background:#efefef;';
const attach = () => {
if (!document.getElementById('rmj-redirect-mask')) {
(document.documentElement || document.body).appendChild(m);
}
};
if (document.documentElement) attach();
else new MutationObserver((_, o) => {
if (document.documentElement) { o.disconnect(); attach(); }
}).observe(document, { childList: true });
})();
(() => {
'use strict';
const TAG = '[RMJ]';
const LOG = (...a) => console.log(TAG, ...a);
const ERR = (...a) => console.error(TAG, ...a);
const HOST = location.hostname;
const isLuogu = () => HOST === 'www.luogu.com.cn';
const isCF = () => HOST === 'codeforces.com';
const isAT = () => HOST === 'atcoder.jp';
const isCfChallenge = () => HOST === 'challenges.cloudflare.com';
const docReady = (fn) =>
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', fn, { once: true })
: fn();
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const waitFor = (selector, cb, timeout = 12000) => {
const t0 = Date.now();
const timer = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(timer);
cb(el);
} else if (Date.now() - t0 > timeout) {
clearInterval(timer);
}
}, 120);
return timer;
};
const waitForEl = (selector, timeout = 10000) =>
new Promise((resolve, reject) => {
const hit = document.querySelector(selector);
if (hit) return resolve(hit);
const root = document.body || document.documentElement;
const obs = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
obs.disconnect();
clearTimeout(timer);
resolve(el);
}
});
obs.observe(root, { childList: true, subtree: true });
const timer = setTimeout(() => {
obs.disconnect();
reject(new Error('超时: ' + selector));
}, timeout);
});
const Timers = () => {
const set = new Set();
return {
iv(fn, ms) { const id = setInterval(fn, ms); set.add(id); return id; },
to(fn, ms) { const id = setTimeout(() => { set.delete(id); fn(); }, ms); set.add(id); return id; },
clear(id) { set.delete(id); clearInterval(id); clearTimeout(id); },
clearAll() { for (const id of set) { clearInterval(id); clearTimeout(id); } set.clear(); }
};
};
// 主题存储
const ThemeStore = (() => {
const P = 'gen-at-';
const get = (k) => GM_getValue(P + k, '');
const set = (k, v) => GM_setValue(P + k, String(v));
// 从洛谷 .theme-page 提取,找不到容器则不写入
const extractFromLuogu = () => {
const el =
document.querySelector('.theme-page.theme-frosted') ||
document.querySelector('.theme-page') ||
document.querySelector('[class*="theme-page"]');
if (!el) return false;
const inlineStyle = (el.getAttribute('style') || '').replace(/"/g, '"');
let computed = null;
try { computed = getComputedStyle(el); } catch { computed = null; }
const readVar = (name) => {
if (inlineStyle) {
const re = new RegExp(
name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*:\\s*([^;]+)'
);
const m = inlineStyle.match(re);
if (m && m[1].trim()) return m[1].trim();
}
if (computed) {
const v = computed.getPropertyValue(name);
if (v && v.trim()) return v.trim();
}
return '';
};
const vars = {
body_back: '--theme-body-back',
bg_image: '--theme-body-image',
bg_color: '--theme-body-color',
bg_repeat: '--theme-body-image-repeat',
bg_size: '--theme-body-image-size',
bg_pos: '--theme-body-image-position',
mid_mask: '--theme-body-mid-mask',
color_filter: '--theme-body-color-filter',
card_bg: '--theme-card-background',
card_blur: '--theme-card-backdrop-filter',
};
let found = false;
for (const [key, cssVar] of Object.entries(vars)) {
const v = readVar(cssVar);
if (v) { set(key, v); found = true; }
}
try {
const nu = document.querySelector('#app .user-nav .title span');
if (nu && nu.textContent.trim()) set('luogu_user', nu.textContent.trim());
const av = document.querySelector('#app .top-bar .avatar img');
if (av && av.src) set('luogu_avatar', av.src);
} catch { /* ignore */ }
return found;
};
const extractWithRetry = (maxAttempts = 25, interval = 200) => {
let n = 0;
const tryIt = () => {
if (extractFromLuogu()) return;
if (++n < maxAttempts) setTimeout(tryIt, interval);
};
tryIt();
};
// 应用到任意遮罩容器
const applyTo = (el) => {
if (!el) return;
const b64 = get('bg_b64');
const raw = get('bg_image');
let img = '';
if (b64) {
img = `url(${b64})`;
} else if (raw) {
const m = raw.match(/url\(\s*(['"]?)(.+?)\1\s*\)/i);
let u = m ? m[2] : raw;
if (u && !/^https?:\/\//i.test(u) && !/^data:/i.test(u)) {
try { u = new URL(u, 'https://www.luogu.com.cn/').href; } catch { /* keep */ }
}
if (u && u !== 'none') img = /^url\(/i.test(raw) ? `url(${u})` : `url(${u})`;
}
const M = {
'--rmj-back': get('body_back') || '#f5f5f5',
'--rmj-img': img,
'--rmj-color': get('bg_color'),
'--rmj-repeat': get('bg_repeat') || 'no-repeat',
'--rmj-size': get('bg_size') || 'cover',
'--rmj-pos': get('bg_pos') || 'center',
'--rmj-mask': get('mid_mask') || 'none',
'--rmj-filter': get('color_filter') || 'none',
'--rmj-card': get('card_bg') || 'rgba(255,255,255,.78)',
'--rmj-blur': get('card_blur') || 'blur(14px)',
};
for (const [k, v] of Object.entries(M)) {
if (v) el.style.setProperty(k, v);
}
};
return { get, set, applyTo, extractFromLuogu, extractWithRetry };
})();
// 共享遮罩样式
GM_addStyle(`
.rmj-ovl-base{
position:fixed!important;inset:0!important;z-index:2147483647!important;
display:flex!important;align-items:center!important;justify-content:center!important;
overflow:hidden!important;pointer-events:all!important;cursor:default!important;
background:var(--rmj-back,#f5f5f5)!important;
color:#333!important;box-sizing:border-box!important;-webkit-font-smoothing:antialiased!important;
}
.rmj-ovl-base *{box-sizing:border-box!important;margin:0!important;padding:0!important;font-family:inherit!important;line-height:inherit!important}
.rmj-ovl-base .rmj-ly{position:absolute!important;inset:0!important;pointer-events:none!important;
-webkit-mask:var(--rmj-mask,none) no-repeat/100% 100%!important;mask:var(--rmj-mask,none) no-repeat/100% 100%!important}
.rmj-ovl-base .rmj-ly-c{background:var(--rmj-color,transparent)!important;filter:var(--rmj-filter,none)!important}
.rmj-ovl-base .rmj-ly-i{background:var(--rmj-img,none) var(--rmj-pos,center)/var(--rmj-size,cover) var(--rmj-repeat,no-repeat)!important}
.rmj-ovl-card{
position:relative!important;z-index:1!important;width:420px!important;max-width:90vw!important;
padding:36px 32px 40px!important;border-radius:4px!important;border:none!important;
background:var(--rmj-card,rgba(255,255,255,.78))!important;
backdrop-filter:var(--rmj-blur,blur(14px))!important;-webkit-backdrop-filter:var(--rmj-blur,blur(14px))!important;
box-shadow:0 8px 32px rgba(0,0,0,.18),0 1.5px 6px rgba(0,0,0,.08)!important;
display:flex!important;flex-direction:column!important;align-items:center!important;gap:20px!important;
}
.rmj-ovl-card h2{font-size:20px!important;font-weight:600!important;color:#333!important;letter-spacing:.5px!important;user-select:none!important;text-align:center!important}
.rmj-ovl-card p{font-size:13px!important;color:#888!important;text-align:center!important;user-select:none!important}
.rmj-ovl-spin{width:26px;height:26px;border:3px solid rgba(0,0,0,.1);border-top-color:#005c97;border-radius:50%;animation:rmj-ovl-spin .7s linear infinite}
@keyframes rmj-ovl-spin{to{transform:rotate(360deg)}}
`);
// 路由
const Router = (() => {
const handlers = new Set();
let last = location.href;
const notify = () => {
const cur = location.href;
if (cur === last) return;
last = cur;
for (const fn of handlers) {
try { fn(cur); } catch (e) { ERR('路由回调异常', e); }
}
};
const wrap = (name) => {
const orig = history[name];
if (!orig || orig.__rmjWrapped) return;
const wrapped = function (...args) {
const ret = orig.apply(this, args);
setTimeout(notify, 0);
return ret;
};
wrapped.__rmjWrapped = true;
history[name] = wrapped;
};
wrap('pushState');
wrap('replaceState');
addEventListener('popstate', notify, true);
addEventListener('hashchange', notify, true);
return {
on(fn) { handlers.add(fn); return () => handlers.delete(fn); },
notify,
};
})();
const CACHE_CF = 'cf-submissions-cache';
const CACHE_AT = 'at-submissions-cache';
const LAST_UPDATE_CF = 'cf-last-update';
const LAST_UPDATE_AT = 'at-last-update';
const SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000;
const FINAL_AT = new Set(['AC', 'WA', 'CE', 'RE', 'TLE', 'MLE', 'OLE', 'IE']);
// CF 挑战页补丁
const CfChallengePatch = {
applied: false,
apply() {
if (this.applied || !isCfChallenge()) return;
this.applied = true;
const XHR = (unsafeWindow || window).XMLHttpRequest;
if (!XHR) return;
const origOpen = XHR.prototype.open;
const origSend = XHR.prototype.send;
XHR.prototype.open = function (method, url) {
this._rmjUrl = url || '';
return origOpen.apply(this, arguments);
};
XHR.prototype.send = function (body) {
if (this._rmjUrl) this.timeout = undefined;
return origSend.apply(this, arguments);
};
}
};
// 洛谷主界面
const MainUI = (() => {
const VERSION = '5.2.2';
const POLL_MS = 1000;
const SVG = {
ok: ``,
err: ``,
na: ``
};
const C = {
ac: '#52c41a', wa: '#e74c3c', na: '#404040',
p: '#0ea5e9', bd: '#e2e8f0', t: '#1e293b',
m: '#64748b', bg: '#fff', alt: '#f8fafc'
};
GM_addStyle(`
.rmj-trigger{display:inline-flex;align-items:center;gap:8px;padding:8px 16px;background:#fff;color:${C.t};border:1px solid ${C.bd};border-radius:8px;font-weight:600;font-size:13px;cursor:pointer;transition:border-color .18s ease,color .18s ease,box-shadow .18s ease;box-shadow:0 1px 3px rgba(0,0,0,.04);user-select:none;margin-right:10px;height:36px}
.rmj-trigger:hover{border-color:${C.p};color:${C.p};box-shadow:0 2px 10px rgba(14,165,233,.14)}
.rmj-trigger:active{box-shadow:0 1px 3px rgba(14,165,233,.18) inset}
.rmj-trigger img{width:20px;height:20px;border-radius:4px}
.rmj-overlay{pointer-events:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:2147483646;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:opacity .22s ease,visibility .22s ease}
.rmj-overlay.show{opacity:1;visibility:visible;pointer-events:auto}
.rmj-modal{width:480px;max-height:85vh;background:rgba(255,255,255,0.82);backdrop-filter:blur(10px);border-radius:12px;box-shadow:0 25px 60px rgba(0,0,0,.14);display:flex;flex-direction:column;overflow:hidden;transform:scale(.97);transition:transform .25s cubic-bezier(.25,.8,.32,1),height .25s ease}
.rmj-overlay.show .rmj-modal{transform:scale(1)}
.rmj-overlay:not(.show) .rmj-modal *{pointer-events:none!important;cursor:default!important}
.rmj-overlay:not(.show) .rmj-modal a{color:inherit!important;text-decoration:none!important}
.rmj-pages{flex:1;overflow:hidden;position:relative;min-height:0;height:auto;transition:height .28s ease}
.rmj-page:not(.active){visibility:hidden;pointer-events:none;position:absolute;top:0;left:50%;width:50%;height:100%}
.rmj-page.active{visibility:visible;pointer-events:auto;opacity:1;z-index:1}
.rmj-head{padding:20px 24px 16px;border-bottom:1px solid ${C.bd};display:flex;align-items:center;gap:12px}
.rmj-head img{width:36px;height:36px;border-radius:8px}
.rmj-title{flex:1;margin:0;font-weight:700;font-size:20px;background:linear-gradient(135deg,${C.p},#2563eb);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.rmj-close{width:32px;height:32px;border:0;background:0 0;color:${C.m};cursor:pointer;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:20px;transition:background .15s ease,color .15s ease}
.rmj-close:hover{background:${C.alt};color:${C.t}}
.rmj-pages{display:grid;width:100%;overflow:hidden;transition:height .28s ease}
.rmj-pages-inner{display:contents}
.rmj-page{grid-area:1/1/2/2;padding:24px;overflow-y:auto;box-sizing:border-box;visibility:hidden;pointer-events:none;opacity:0;transition:opacity .25s ease,visibility .25s}
.rmj-welcome-title{font-size:18px;font-weight:700;color:${C.t};margin-bottom:12px}
.rmj-update-badge{display:inline-block;padding:3px 10px;background:linear-gradient(135deg,${C.p},#2563eb);color:#fff;border-radius:20px;font-size:11px;font-weight:600;margin-left:8px}
.rmj-feat{list-style:none;padding:0;margin:16px 0}
.rmj-feat li{display:flex;align-items:flex-start;gap:10px;padding:8px 0;color:${C.m};font-size:14px;line-height:1.5}
.rmj-feat li::before{content:'·';flex-shrink:0}
.rmj-sec{font-size:11px;font-weight:700;color:${C.m};text-transform:uppercase;letter-spacing:1px;margin:0 0 12px}
.rmj-sec:not(:first-child){margin-top:20px}
.rmj-card{background:${C.alt};border:1px solid ${C.bd};border-radius:10px;padding:14px 16px;display:flex;align-items:center;justify-content:space-between;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease}
.rmj-card:hover{border-color:rgba(14,165,233,.35);box-shadow:0 2px 10px rgba(14,165,233,.08);background:#fff}
.rmj-card+.rmj-card{margin-top:8px}
.rmj-info{display:flex;align-items:center;gap:12px}
.rmj-logo{width:32px;height:32px;border-radius:6px;object-fit:contain}
.rmj-name{font-size:14px;font-weight:600;color:${C.t}}
.rmj-badge{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600}
.rmj-badge.on{background:rgba(82,196,26,.09);color:${C.ac}}
.rmj-badge.off{background:rgba(231,76,60,.09);color:${C.wa};cursor:pointer;text-decoration:none;transition:background .15s ease,color .15s ease}
.rmj-badge.off:hover{background:rgba(231,76,60,.16)}
.rmj-badge.load{background:#f1f5f9;color:${C.m}}
.rmj-spin{width:14px;height:14px;border:2px solid ${C.bd};border-top-color:${C.p};border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.rmj-links{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px}
.rmj-link{display:flex;align-items:center;gap:8px;padding:10px 12px;background:${C.alt};border:1px solid ${C.bd};border-radius:8px;color:${C.t};text-decoration:none;font-size:13px;font-weight:500;transition:border-color .18s ease,color .18s ease,box-shadow .18s ease,background .18s ease}
.rmj-link:hover{border-color:${C.p};color:${C.p};background:#fff;box-shadow:0 2px 10px rgba(14,165,233,.10)}
.rmj-link svg{width:16px;height:16px;flex-shrink:0}
.rmj-foot{padding:16px 24px;border-top:1px solid ${C.bd};display:flex;align-items:center;justify-content:space-between;background:${C.alt}}
.rmj-btn{padding:8px 16px;border:1px solid ${C.bd};background:#fff;color:${C.t};border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:border-color .15s ease,color .15s ease,background .15s ease}
.rmj-btn:hover:not(:disabled){border-color:${C.p};color:${C.p};background:#f8fafc}
.rmj-btn:disabled{opacity:.4;cursor:not-allowed}
.rmj-dots{display:flex;gap:6px}
.rmj-dot{width:6px;height:6px;border-radius:50%;background:${C.bd};transition:background .2s ease,width .2s ease}
.rmj-dot.active{background:${C.p};width:18px;border-radius:3px}
.rmj-prob{display:inline-flex;align-items:center;transition:color .3s ease}
.rmj-prob.ac{color:${C.ac};font-weight:bold}
.rmj-prob.wa{color:${C.wa};font-weight:bold}
.rmj-prob.na{color:#3498db}
.rmj-ico{position:absolute;inset:0;background:0 0;display:flex;align-items:center;justify-content:center;z-index:10;border-radius:inherit;animation:fadeIn .3s}
.rmj-ico svg{width:18px;height:18px;animation:popIn .3s cubic-bezier(.34,1.56,.64,1)}
.rmj-hide>*:not(.rmj-ico){visibility:hidden!important}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
@keyframes popIn{from{transform:scale(.5);opacity:0}to{transform:scale(1);opacity:1}}
@keyframes badgePop{0%{transform:scale(.8);opacity:0}60%{transform:scale(1.05)}100%{transform:scale(1);opacity:1}}
.rmj-record-badge{animation:badgePop .4s ease-out}
.rmj-sel{position:relative;display:inline-block;margin-left:8px}
.rmj-sel-btn{position:relative;display:inline-flex;align-items:center;min-width:120px;padding:.3125em 1.5em .3125em 1em;border:1px solid #ccc;border-radius:3px;background:#fff;font-size:14px;line-height:1.4;cursor:pointer;text-align:left}
.rmj-sel-btn::after{content:"";position:absolute;right:10px;top:50%;width:6px;height:6px;border-right:1.5px solid #555;border-bottom:1.5px solid #555;transform:translateY(-65%) rotate(45deg);transition:transform .2s}
.rmj-sel.open .rmj-sel-btn::after{transform:translateY(-35%) rotate(-135deg)}
.rmj-sel-menu{position:absolute;top:calc(100% + 4px);left:0;width:100%;margin:0;padding:4px 0;list-style:none;background:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 4px 12px rgba(0,0,0,.1);opacity:0;transform:translateY(-8px);pointer-events:none;transition:opacity .2s ease,transform .2s ease;z-index:100}
.rmj-sel-menu.open{opacity:1;transform:translateY(0);pointer-events:all}
.rmj-sel-menu li{padding:3px 12px;cursor:pointer;font-size:14px}
.rmj-sel-menu li:hover{background:#f0f9ff}
.rmj-refresh-wrap{display:inline-flex;align-items:center;margin-left:8px;cursor:pointer;color:${C.m};transition:color .18s ease,transform .25s ease}
.rmj-refresh-wrap:hover{color:${C.p};transform:rotate(90deg)}
.rmj-refresh{width:14px;height:14px}
.rmj-switch{position:relative;display:inline-block;width:40px;height:22px}
.rmj-switch input{opacity:0;width:0;height:0}
.rmj-slider{position:absolute;cursor:pointer;inset:0;background:#ccc;transition:background .2s ease;border-radius:22px}
.rmj-slider:before{position:absolute;content:"";height:16px;width:16px;left:3px;bottom:3px;background:#fff;transition:transform .2s ease;border-radius:50%}
.rmj-switch input:checked + .rmj-slider{background:${C.p}}
.rmj-switch input:checked + .rmj-slider:before{transform:translateX(18px)}
.rmj-switch input:disabled + .rmj-slider{opacity:.5;cursor:not-allowed}
`);
const cfMap = new Map();
const atMap = new Map();
let cfRawCache = '';
let atRawCache = '';
let atCapturing = false;
let listTimer = null;
let problemTimer = null;
let problemObserver = null;
let problemGen = 0;
let currentProblemPath = '';
let modalEl = null;
let modalPage = 0;
let cfPollTimer = null;
let atLoginPollTimer = null;
const SPECIAL_ACCOUNTS = ['General0000000826', 'General000000826'];
const isSpecialAccount = () => {
const cfH = GM_getValue('cf-handle', '');
const atH = GM_getValue('at-handle', '');
return SPECIAL_ACCOUNTS.includes(cfH) || SPECIAL_ACCOUNTS.includes(atH);
};
const isSyncDisabled = () => {
if (isSpecialAccount()) return true;
let fromParams = false;
try {
const params = new URLSearchParams(location.search);
fromParams = params.get('rmj-disable-sync') === 'true';
} catch (e) { /* ignore */ }
if (fromParams) return true;
if (GM_getValue('rmj-disable-sync', null) === null) {
GM_setValue('rmj-disable-sync', false);
}
return GM_getValue('rmj-disable-sync', false) === true;
};
const captureCfHandle = () => {
if (!isCF()) return;
const attempt = () => {
const el = document.querySelector('#header > div.lang-chooser > div:nth-child(2) > a:nth-child(1)');
if (!el) return setTimeout(attempt, 1000);
const text = el.textContent.trim();
if (text && !['Enter', '登录'].includes(text)) {
const stored = GM_getValue('cf-handle', '');
if (stored !== text) GM_setValue('cf-handle', text);
}
};
attempt();
};
// 拉取 AT 用户名;未登录返回 null
const fetchAtHandle = () => new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://atcoder.jp/',
timeout: 8000,
onload: (r) => {
if (r.status !== 200) return resolve(null);
try {
const doc = new DOMParser().parseFromString(r.responseText, 'text/html');
const el = doc.querySelector('#navbar-collapse > ul.nav.navbar-nav.navbar-right > li:nth-child(2) > a');
if (!el) return resolve(null);
const t = el.textContent.trim();
if (!t || ['Sign Up', 'Login', '登录'].includes(t)) return resolve(null);
resolve(t);
} catch { resolve(null); }
},
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
const captureAtHandle = async () => {
atCapturing = true;
updateModal();
const handle = await fetchAtHandle();
if (handle) {
const stored = GM_getValue('at-handle', '');
if (stored !== handle) GM_setValue('at-handle', handle);
atCapturing = false;
return updateModal();
}
// 未登录:打开登录页并 5s 轮询
try { window.open('https://atcoder.jp/login', '_blank'); } catch { /* ignore */ }
if (atLoginPollTimer) clearInterval(atLoginPollTimer);
let polls = 0;
atLoginPollTimer = setInterval(async () => {
polls++;
if (polls > 60) {
clearInterval(atLoginPollTimer);
atLoginPollTimer = null;
atCapturing = false;
updateModal();
return;
}
const h = await fetchAtHandle();
if (h) {
clearInterval(atLoginPollTimer);
atLoginPollTimer = null;
const stored = GM_getValue('at-handle', '');
if (stored !== h) GM_setValue('at-handle', h);
atCapturing = false;
updateModal();
}
}, 5000);
};
const readCaches = () => {
try {
const cfData = GM_getValue(CACHE_CF, '[]');
const atData = GM_getValue(CACHE_AT, '[]');
if (cfData !== cfRawCache) {
cfRawCache = cfData;
cfMap.clear();
JSON.parse(cfData).forEach((s) => {
const m = s?.taskDisplay?.match(/CF[A-Z0-9]+/i);
if (!m) return;
const key = m[0].toUpperCase();
const status = s.status || null;
if (!cfMap.has(key) || (cfMap.get(key) !== 'OK' && status === 'OK')) {
cfMap.set(key, status);
}
});
}
if (atData !== atRawCache) {
atRawCache = atData;
atMap.clear();
JSON.parse(atData).forEach((s) => {
if (!s?.taskKey) return;
const key = s.taskKey.toLowerCase();
const status = s.status || null;
if (!atMap.has(key) || (atMap.get(key) !== 'AC' && status === 'AC')) {
atMap.set(key, status);
}
});
}
} catch (e) { ERR(e); }
};
const queryStatus = (pid) => {
if (/^CF[A-Z0-9]+$/i.test(pid)) {
const v = cfMap.get(pid.toUpperCase());
return v === 'OK' ? 'AC' : v ? 'WA' : 'NONE';
}
if (/^AT_/i.test(pid)) {
const k = pid.replace(/^AT_/i, '').toLowerCase();
const v = atMap.get(k);
return v === 'AC' ? 'AC' : v ? 'WA' : 'NONE';
}
return null;
};
const updateListIcons = () => {
readCaches();
document.querySelectorAll('.row').forEach((row) => {
const [sc, pc] = row.children;
if (!pc) return;
const m = pc.textContent.trim().match(/^(\S+)/);
if (!m) return;
const st = queryStatus(m[1]);
if (!st) {
const ov = sc.querySelector('.rmj-ico');
if (ov) { ov.remove(); sc.classList.remove('rmj-hide'); }
return;
}
let ov = sc.querySelector('.rmj-ico');
if (!ov) {
ov = document.createElement('div');
ov.className = 'rmj-ico';
sc.style.position = 'relative';
sc.appendChild(ov);
sc.classList.add('rmj-hide');
}
const icon = st === 'AC' ? SVG.ok : st === 'WA' ? SVG.err : SVG.na;
if (ov.dataset.s !== st) { ov.innerHTML = icon; ov.dataset.s = st; }
});
};
const startListPoll = () => {
if (listTimer) return;
updateListIcons();
listTimer = setInterval(updateListIcons, POLL_MS);
};
const stopListPoll = () => {
if (!listTimer) return;
clearInterval(listTimer);
listTimer = null;
};
const updateProblemBadge = () => {
readCaches();
const pid = location.pathname.split('/problem/')[1];
if (!pid) return;
const st = queryStatus(pid);
if (!st) return;
const link = document.querySelectorAll('a[href*="/record/list?pid="]')[0];
if (!link) return;
let tag = link.querySelector('.rmj-prob');
if (!tag) {
const orig = link.querySelector('span');
if (orig) orig.style.display = 'none';
tag = document.createElement('span');
tag.className = 'rmj-prob';
link.appendChild(tag);
}
const way = document.querySelectorAll(
'a[href*="https://help.luogu.com.cn/manual/luogu/problem/remote-judge"]'
)[0];
if (way && way.textContent !== 'GenGen RMJ') {
way.textContent = 'GenGen RMJ';
way.href = 'https://gengen.qzz.io/rmj/';
}
const cls = st === 'AC' ? 'ac' : st === 'WA' ? 'wa' : 'na';
const txt = st === 'AC' ? 'Accepted' : st === 'WA' ? 'Unaccepted' : '暂无';
if (tag.dataset.s !== st) {
tag.className = 'rmj-prob ' + cls;
tag.innerHTML = txt;
tag.dataset.s = st;
}
};
const startProblemPoll = () => {
if (problemTimer) return;
updateProblemBadge();
problemTimer = setInterval(updateProblemBadge, POLL_MS);
};
const stopProblemPoll = () => {
if (!problemTimer) return;
clearInterval(problemTimer);
problemTimer = null;
};
const teardownProblemObserver = () => {
problemGen++;
if (problemObserver) { problemObserver.disconnect(); problemObserver = null; }
currentProblemPath = '';
};
const startCfPoll = () => {
if (cfPollTimer) clearInterval(cfPollTimer);
const initialHandle = GM_getValue('cf-handle', '');
let checks = 0;
cfPollTimer = setInterval(() => {
checks++;
const current = GM_getValue('cf-handle', '');
if (current !== initialHandle || checks > 60) {
clearInterval(cfPollTimer);
cfPollTimer = null;
updateModal();
}
}, 1000);
updateModal();
};
const buildModal = () => {
if (modalEl) modalEl.remove();
modalEl = document.createElement('div');
modalEl.className = 'rmj-overlay';
modalEl.innerHTML = `
GenGen RMJ
👋 欢迎使用 GenGen RMJ 5 正式版 v${VERSION}
在洛谷上无缝刷 Codeforces 和 AtCoder 题目,同步提交记录。
- 请点击下一页来绑定你的账户!
- 自动同步 CF/AT 账号提交记录
- 题目列表和题单直观显示通过状态
- Cloudflare 验证加速绕过
- 记录详细查看功能
- 最新更新:完全适配新前端
账号绑定

Codeforces

AtCoder
设置
快捷链接
`;
document.body.appendChild(modalEl);
modalEl.querySelector('.rmj-close').onclick = () => {
GM_setValue('rmj-version-shown', VERSION);
hideModal();
modalEl.addEventListener('click', (e) => {
if (!modalEl.classList.contains('show')) {
e.stopImmediatePropagation();
e.preventDefault();
}
}, true);
};
modalEl.querySelector('.rmj-prev').onclick = () => switchModalPage(0);
modalEl.querySelector('.rmj-next').onclick = () => switchModalPage(1);
updateModal();
};
const updateModal = () => {
if (!modalEl) return;
const cfH = GM_getValue('cf-handle', '');
const atH = GM_getValue('at-handle', '');
const cfEl = modalEl.querySelector('#rmj-cf');
const atEl = modalEl.querySelector('#rmj-at');
const refreshIcon = ``;
if (cfPollTimer) {
cfEl.innerHTML = `同步中...`;
} else if (cfH) {
cfEl.innerHTML = `${cfH}${refreshIcon}`;
} else {
cfEl.innerHTML = `去绑定 →`;
}
if (atCapturing) {
atEl.innerHTML = `等待登录...`;
} else if (atH) {
atEl.innerHTML = `${atH}${refreshIcon}`;
} else {
atEl.innerHTML = `去绑定 →`;
}
const cfBind = modalEl.querySelector('#rmj-cf-bind');
if (cfBind) cfBind.onclick = () => { startCfPoll(); };
const cfRefresh = modalEl.querySelector('#rmj-cf .rmj-refresh-wrap');
if (cfRefresh) {
cfRefresh.onclick = () => {
window.open('https://codeforces.com/enter', '_blank');
startCfPoll();
};
}
const atBind = modalEl.querySelector('#rmj-at-bind');
if (atBind) {
atBind.onclick = (e) => { e.preventDefault(); captureAtHandle(); };
}
const atRefresh = modalEl.querySelector('#rmj-at .rmj-refresh-wrap');
if (atRefresh) atRefresh.onclick = () => captureAtHandle();
const syncToggle = modalEl.querySelector('#rmj-disable-sync');
if (syncToggle) {
const isDisabled = isSyncDisabled();
syncToggle.checked = !isDisabled;
if (isSpecialAccount()) {
syncToggle.checked = false;
syncToggle.disabled = true;
syncToggle.title = '特殊账号无法修改同步设置';
}
syncToggle.onchange = async () => {
if (isSpecialAccount()) {
syncToggle.checked = false;
return;
}
GM_setValue(CACHE_AT, '[]');
GM_setValue(CACHE_CF, '[]');
GM_setValue(LAST_UPDATE_AT, '0');
GM_setValue('cf-0last-update', '0');
updateListIcons();
updateProblemBadge();
};
}
};
const switchModalPage = (idx) => {
if (!modalEl || idx === modalPage) return;
const pagesContainer = modalEl.querySelector('.rmj-pages');
const allPages = modalEl.querySelectorAll('.rmj-page');
const targetPage = allPages[idx];
const dots = modalEl.querySelectorAll('.rmj-dot');
const prevBtn = modalEl.querySelector('.rmj-prev');
const nextBtn = modalEl.querySelector('.rmj-next');
allPages.forEach((p, i) => p.classList.toggle('active', i === idx));
if (targetPage && pagesContainer) {
pagesContainer.style.height = targetPage.scrollHeight + 'px';
}
dots.forEach((d, i) => d.classList.toggle('active', i === idx));
prevBtn.disabled = idx === 0;
nextBtn.disabled = idx === dots.length - 1;
modalPage = idx;
if (idx === 1 && !GM_getValue('at-handle', '') && !atCapturing) {
fetchAtHandle().then((h) => {
if (h) {
const stored = GM_getValue('at-handle', '');
if (stored !== h) GM_setValue('at-handle', h);
updateModal();
}
});
}
};
const showModal = (page = 0) => {
if (!modalEl) buildModal();
modalPage = page;
const pagesContainer = modalEl.querySelector('.rmj-pages');
const allPages = modalEl.querySelectorAll('.rmj-page');
const targetPage = allPages[page];
const dots = modalEl.querySelectorAll('.rmj-dot');
const prevBtn = modalEl.querySelector('.rmj-prev');
const nextBtn = modalEl.querySelector('.rmj-next');
allPages.forEach((p, i) => p.classList.toggle('active', i === page));
requestAnimationFrame(() => {
if (targetPage && pagesContainer) {
pagesContainer.style.height = targetPage.scrollHeight + 'px';
}
});
dots.forEach((d, i) => d.classList.toggle('active', i === page));
prevBtn.disabled = page === 0;
nextBtn.disabled = page === dots.length - 1;
modalEl.style.pointerEvents = '';
modalEl.classList.add('show');
};
const hideModal = () => {
if (!modalEl) return;
modalEl.classList.remove('show');
modalEl.style.pointerEvents = 'none';
};
const checkVersion = () => {
if (GM_getValue('rmj-version-shown', '') !== VERSION) {
setTimeout(() => showModal(0), 500);
}
};
const injectHomeButton = () => {
if (location.pathname !== '/' || document.querySelector('.rmj-trigger')) return;
waitFor('.right', (nav) => {
if (nav.dataset.rmj) return;
nav.dataset.rmj = '1';
const btn = document.createElement('button');
btn.className = 'rmj-trigger';
btn.innerHTML = `
GenGen RMJ`;
btn.onclick = () => showModal(1);
nav.parentElement.insertBefore(btn, nav);
});
};
const initProblemPage = () => {
const p = location.pathname;
if (!p.startsWith('/problem/CF') && !p.startsWith('/problem/AT')) return;
if (currentProblemPath === p && problemObserver) return;
currentProblemPath = p;
const myPath = p;
const myGen = ++problemGen;
const rmj = p.startsWith('/problem/CF') ? '1' : '2';
const pid = p.split('/problem/')[1];
if (!pid) return;
if (problemObserver) { problemObserver.disconnect(); problemObserver = null; }
waitFor('.side', (side) => {
if (myGen !== problemGen || currentProblemPath !== myPath) return;
problemObserver = new MutationObserver(() => {
side.querySelectorAll('a[href*="/record/list?pid="]').forEach((link) => {
if (link.dataset.rmjHook) return;
link.dataset.rmjHook = '1';
link.addEventListener('click', (e) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopImmediatePropagation();
const u = new URL('/record/list', location.origin);
const sp = new URLSearchParams(location.search);
for (const [k, v] of sp) u.searchParams.set(k, v);
u.searchParams.set('pid', pid);
u.searchParams.set('rmj', rmj);
location.href = u.toString();
}, true);
link.addEventListener('mousedown', (e) => {
if (e.button !== 1) return;
const u = new URL('/record/list', location.origin);
const sp = new URLSearchParams(location.search);
for (const [k, v] of sp) u.searchParams.set(k, v);
u.searchParams.set('pid', pid);
u.searchParams.set('rmj', rmj);
link.href = u.toString();
}, true);
});
updateProblemBadge();
});
problemObserver.observe(side, { childList: true, subtree: true });
});
updateProblemBadge();
startProblemPoll();
};
const injectRecordListSelector = () => {
if (!location.pathname.startsWith('/record/list')) return;
const myGen = problemGen;
waitFor(
'#app > div.main-container.lside-nav > div > main > div > div:nth-child(1) > section:nth-child(2) > div > div > div.text.lform-size-middle',
(b) => {
if (myGen !== problemGen) return;
const cur = new URLSearchParams(location.search).get('rmj') || '';
let w = document.querySelector('.rmj-sel');
if (w) {
const btn = w.querySelector('.rmj-sel-btn');
const menu = w.querySelector('.rmj-sel-menu');
if (btn && menu) {
const act = menu.querySelector(`li[data-value="${cur}"]`);
if (act && btn.textContent !== act.textContent) {
btn.textContent = act.textContent;
}
}
return;
}
w = document.createElement('span');
w.className = 'rmj-sel';
w.innerHTML = `
`;
b.after(w);
const btn = w.querySelector('.rmj-sel-btn');
const menu = w.querySelector('.rmj-sel-menu');
btn.addEventListener('click', (e) => {
e.stopPropagation();
w.classList.toggle('open');
menu.classList.toggle('open');
});
menu.querySelectorAll('li').forEach((li) => {
li.addEventListener('click', (e) => {
e.stopPropagation();
const val = li.dataset.value;
btn.textContent = li.textContent;
w.classList.remove('open');
menu.classList.remove('open');
const p = new URLSearchParams(location.search);
if (val) p.set('rmj', val);
else p.delete('rmj');
location.search = p.toString();
});
});
document.addEventListener('click', () => {
w.classList.remove('open');
menu.classList.remove('open');
});
const act = menu.querySelector(`li[data-value="${cur}"]`);
if (act) btn.textContent = act.textContent;
}
);
};
const handleRoute = () => {
if (!isLuogu()) return;
const path = location.pathname;
ThemeStore.extractWithRetry(15, 250);
if (path === '/') injectHomeButton();
if (path.startsWith('/problem/CF') || path.startsWith('/problem/AT')) {
initProblemPage();
} else {
stopProblemPoll();
teardownProblemObserver();
}
if (path.startsWith('/training') || path.startsWith('/problem/list')) {
startListPoll();
} else {
stopListPoll();
}
if (path.startsWith('/record/list')) {
injectRecordListSelector();
}
};
const init = () => {
if (!isLuogu()) return;
checkVersion();
handleRoute();
addEventListener('beforeunload', () => { stopListPoll(); stopProblemPoll(); });
};
return { init, handleRoute, hideModal, captureCfHandle };
})();
// 洛谷记录详情页(DET)
const RecordDetail = (() => {
const _gmGet = GM_getValue;
const _gmReq = GM_xmlhttpRequest;
const VALID = [
/^https:\/\/www\.luogu\.com\.cn\/record\/cf\/[^/]+\/[^/?#]+/,
/^https:\/\/www\.luogu\.com\.cn\/record\/at\/[^/]+\/[^/?#]+/,
];
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']);
GM_addStyle(`
#rmj-det-overlay.rmj-ovl-base{z-index:2147483647!important}
#rmj-det-card{width:420px!important;max-width:90vw!important}
`);
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('网络错误')),
ontimeout: () => no(new Error('请求超时'))
});
});
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('状态码 ' + r.status)),
onerror: () => no(new Error('网络错误')),
ontimeout: () => no(new Error('请求超时'))
});
});
let lastKey = '';
let generation = 0;
const timers = Timers();
const buildLoader = () => {
const el = document.createElement('div');
el.id = 'rmj-det-overlay';
el.className = 'rmj-ovl-base';
el.innerHTML = `
`;
ThemeStore.applyTo(el);
return el;
};
// 未绑定提示
const showBindError = (bindName) => {
const el = document.createElement('div');
el.id = 'rmj-det-overlay';
el.className = 'rmj-ovl-base';
el.innerHTML = `
账号未绑定
请先在 GenGen RMJ 面板中绑定 ${bindName} 账号
前往洛谷主页绑定
`;
ThemeStore.applyTo(el);
(document.documentElement || document.body).appendChild(el);
};
const destroy = () => {
generation++;
timers.clearAll();
const mask = document.getElementById('rmj-det-overlay');
if (mask) mask.remove();
lastKey = '';
};
const init = () => {
if (!VALID.some((r) => r.test(location.href))) {
if (lastKey) destroy();
return;
}
const m = location.pathname.match(/\/record\/(cf|at)\/([^/]+)\/([^/?#]+)/);
if (!m) return;
const [, plat, cid, sid] = m;
const key = `${plat}/${cid}/${sid}`;
if (lastKey === key) return;
destroy();
lastKey = key;
const myGen = generation;
const isCFPage = plat === 'cf';
const isATPage = plat === 'at';
// 未绑定则拒绝查看
const bindKey = isCFPage ? 'cf-handle' : 'at-handle';
const bindName = isCFPage ? 'Codeforces' : 'AtCoder';
if (!_gmGet(bindKey, '')) {
showBindError(bindName);
return;
}
const handle = _gmGet('cf-handle', '') || _gmGet('rmj-cf-handle', '') || '';
const code = _gmGet(`{${cid},${sid}}`, '');
const titleTimer = timers.iv(() => { document.title = 'GenGen RMJ 加载中'; }, 100);
const mask = buildLoader();
(document.documentElement || document.body || document).appendChild(mask);
let cachedTpl = null;
const fetchTemplate = async () => {
if (cachedTpl) return cachedTpl;
return new Promise((ok, no) => {
_gmReq({
method: 'GET', url: 'https://www.luogu.com.cn/record/290510067', timeout: 15000,
onload: (r) => {
if (r.status === 200) { cachedTpl = r.responseText; ok(cachedTpl); }
else no(new Error('模板获取失败'));
},
onerror: () => no(new Error('模板网络错误')),
ontimeout: () => no(new Error('模板请求超时'))
});
});
};
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 doc = new DOMParser().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 [];
const 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
}];
}
function buildHtml(tpl, sub) {
const FE_RE = /`);
}
async function renderPage(sub) {
if (myGen !== generation) return;
const tpl = await fetchTemplate().catch(() => null);
if (!tpl || myGen !== generation) return;
const doc = new DOMParser().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);
});
}
(async () => {
const updateCache = (sub) => {
if (!sub || myGen !== generation) return;
if (isCFPage) {
let cache = [];
try { cache = JSON.parse(GM_getValue(CACHE_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_CF, JSON.stringify(cache));
GM_setValue(LAST_UPDATE_CF, Date.now());
} else if (isATPage) {
let cache = [];
try { cache = JSON.parse(GM_getValue(CACHE_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 taskKey = `${cid}_${sub.problem.index}`;
const item = {
id: String(sid), 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_AT, JSON.stringify(cache));
GM_setValue(LAST_UPDATE_AT, Date.now());
}
};
const cfDomainPath = Number(cid) > 100000 ? 'gym' : 'contest';
const origUrl = isCFPage
? `https://codeforces.com/${cfDomainPath}/${cid}/submission/${sid}`
: `https://atcoder.jp/contests/${cid}/submissions/${sid}`;
let sub = null;
try {
sub = isCFPage ? await findCF() : (isATPage ? await findAT() : null);
} catch (e) {
if (myGen !== generation) return;
if (isATPage) { timers.clear(titleTimer); location.replace(origUrl); return; }
}
if (myGen !== generation) return;
if (isCFPage && sub === null) {
timers.clear(titleTimer);
location.replace(origUrl);
return;
}
if (sub) updateCache(sub);
timers.clear(titleTimer);
// 已拿到结果直接渲染一次
await renderPage(sub);
if (myGen !== generation) return;
let currentSub = sub;
let atWaitPollingId = null;
timers.iv(() => {
if (myGen !== generation) return;
const m2 = document.getElementById('rmj-det-overlay');
if (m2 && (document.querySelector('#app') || document.querySelector('.lfe-body'))) m2.remove();
const hasCode = (isATPage && 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;
});
}
}
// AT 未返回完整测试点时的等待提示
if (atWaitPollingId !== null) {
const h5 = document.querySelector(
'#app > div.main-container.lside-nav > div > main > div > div > div.main > div > section > div > h5'
);
if (h5 && h5.textContent !== '正在等待 AtCoder 评测完成') {
h5.textContent = '正在等待 AtCoder 评测完成';
}
const testWrap = document.querySelector('.test-case-wrap');
if (testWrap && !document.getElementById('rmj-at-wait-msg')) {
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 ((!isCFPage && !isATPage) || !sub) return;
const startAtWaitPolling = () => {
if (atWaitPollingId !== null || !isATPage) return;
atWaitPollingId = timers.iv(async () => {
if (myGen !== generation) return;
try {
const s = await findAT();
if (myGen !== generation) return;
if (s && s.cases.length > 0) {
timers.clear(atWaitPollingId);
atWaitPollingId = null;
const msg = document.getElementById('rmj-at-wait-msg');
if (msg) msg.remove();
currentSub = s;
await renderPage(s);
updateCache(s);
}
} catch (e) { /* ignore */ }
}, 5000);
};
if (isATPage && 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 = timers.iv(async () => {
if (myGen !== generation) return;
if (polling) return;
polling = true;
try {
const s = isCFPage ? await findCF() : await findAT();
if (myGen !== generation) return;
if (s) {
currentSub = 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)) {
timers.clear(intervalId);
await renderPage(s);
}
}
} catch (e) { /* ignore */ }
finally { polling = false; }
}, 3000);
}
})();
};
return { init, destroy };
})();
// 洛谷记录列表页
const RecordList = (() => {
let syncStarted = false;
let pageKey = '';
let pageGen = 0;
const timers = Timers();
const req = (url, opts = {}) => new Promise((res, rej) => {
const t = setTimeout(() => rej(new Error('请求超时')), 8000);
GM_xmlhttpRequest({
method: 'GET', url, ...opts,
onload: (r) => { clearTimeout(t); res(r); },
onerror: (e) => { clearTimeout(t); rej(e); }
});
});
const getCache = (key) => {
try {
const p = JSON.parse(GM_getValue(key, '[]'));
if (!Array.isArray(p)) return [];
return p.map((s) => {
if (s.submitUrl) {
let m = s.submitUrl.match(/atcoder\.jp\/contests\/([^/]+)\/submissions\/(\d+)/);
if (m) s.submitUrl = `https://www.luogu.com.cn/record/at/${m[1]}/${m[2]}`;
else {
m = s.submitUrl.match(/codeforces\.com\/(?:contest|gym)\/(\d+)\/submission\/(\d+)/);
if (m) s.submitUrl = `https://www.luogu.com.cn/record/cf/${m[1]}/${m[2]}`;
}
}
if (s.luoguUrl) {
let m = s.luoguUrl.match(/atcoder\.jp\/contests\/[^/]+\/tasks\/([^/\s?#]+)/);
if (m) s.luoguUrl = `https://www.luogu.com.cn/problem/AT_${m[1]}`;
else {
m = s.luoguUrl.match(/codeforces\.com\/(?:problemset\/problem|(?:contest|gym)\/\d+\/problem)\/(\d+)\/([A-Za-z]\d?)/);
if (m) s.luoguUrl = `https://www.luogu.com.cn/problem/CF${m[1]}${m[2]}`;
}
}
return s;
});
} catch { return []; }
};
const saveCache = (key, c) => GM_setValue(key, JSON.stringify(Array.isArray(c) ? c : []));
const markUpdated = (key) => GM_setValue(key, Date.now());
const crawlATContest = async (contestId, existingCache = []) => {
if (!contestId) return existingCache;
try {
const r = await req(`https://atcoder.jp/contests/${contestId}/submissions/me`);
if (r.status !== 200) return existingCache;
const doc = new DOMParser().parseFromString(r.responseText, 'text/html');
const rows = Array.from(doc.querySelectorAll('table.table tbody tr'));
if (!rows.length) return existingCache;
const cacheMap = new Map(existingCache.map((s) => [s.id, s]));
let hasNew = false;
for (const row of rows) {
const cells = Array.from(row.querySelectorAll('td'));
if (cells.length < 7) continue;
const tl = cells[1]?.querySelector('a');
const tHref = tl?.href || '';
const tName = tl?.innerText?.trim() || '';
const tKey = tHref.match(/\/tasks\/([^/\s]+)/)?.[1] || '';
let rs = 'Waiting';
for (const c of cells) {
const l = c.querySelector('.label');
if (l) { rs = l.innerText.trim(); break; }
}
const da = cells[cells.length - 1]?.querySelector('a.submission-details-link');
if (!da?.href) continue;
const sid = da.href.split('/').pop();
const isF = FINAL_AT.has(rs);
const prev = cacheMap.get(sid);
if (!prev || prev.status !== rs) {
cacheMap.set(sid, {
id: sid, taskKey: tKey,
taskDisplay: `${tKey} - ${tName}`,
status: rs,
submitUrl: `https://www.luogu.com.cn/record/at/${contestId}/${sid}`,
luoguUrl: `https://www.luogu.com.cn/problem/AT_${tKey}`,
timestamp: prev?.timestamp || Date.now(),
isFinal: isF
});
hasNew = true;
} else if (isF && !prev.isFinal) {
prev.isFinal = true;
prev.status = rs;
hasNew = true;
}
}
if (hasNew) {
return Array.from(cacheMap.values()).sort((a, b) => Number(b.id) - Number(a.id));
}
return existingCache;
} catch (e) {
LOG('AT 爬取失败', e.message || e);
return existingCache;
}
};
const fetchCFData = async (handle) => {
const sr = await req(`https://codeforces.com/api/user.status?handle=${encodeURIComponent(handle)}&from=1`);
if (sr.status !== 200) throw new Error('CF API 状态码非 200');
const d = JSON.parse(sr.responseText);
if (d.status !== 'OK') throw new Error('CF API 状态非 OK');
const seen = new Set();
return (d.result || []).filter((s) => {
if (seen.has(s.id)) return false;
seen.add(s.id);
return true;
}).map((s) => ({
id: String(s.id),
taskDisplay: `CF${s.contestId}${s.problem.index} - ${s.problem.name}`,
status: s.verdict || 'Waiting',
timestamp: s.creationTimeSeconds * 1000,
submitUrl: `https://www.luogu.com.cn/record/cf/${s.contestId}/${s.id}`,
luoguUrl: `https://www.luogu.com.cn/problem/CF${s.contestId}${s.problem.index}`,
passedTestCount: s.passedTestCount
}));
};
const fetchATProblems = async () => {
try {
const r = await req('https://kenkoooo.com/atcoder/resources/problems.json');
if (r.status !== 200) return new Map();
const m = new Map();
for (const p of JSON.parse(r.responseText)) {
if (p.id) m.set(p.id, { index: p.problem_index || '', name: p.name || '' });
}
return m;
} catch { return new Map(); }
};
const fmtATTask = (pid, pMap) => {
const p = pMap.get(pid);
if (p?.name && p.index) return `${pid} - ${p.index} - ${p.name}`;
const parts = pid.split('_');
const c = parts[0] || '';
const i = parts[1] ? parts[1].toUpperCase() : '';
return `${pid} - ${i ? `${c.toUpperCase()} ${i}` : pid}`;
};
const fetchATSubmissions = async (pMap, uid) => {
try {
if (!uid) return [];
const sr = await req(`https://kenkoooo.com/atcoder/atcoder-api/v3/user/submissions?user=${encodeURIComponent(uid)}&from_second=0`);
if (sr.status !== 200) return [];
const subs = JSON.parse(sr.responseText);
if (!Array.isArray(subs)) return [];
return subs.map((s) => ({
id: String(s.id),
taskKey: s.problem_id,
taskDisplay: fmtATTask(s.problem_id, pMap),
status: s.result,
submitUrl: `https://www.luogu.com.cn/record/at/${s.contest_id}/${s.id}`,
luoguUrl: `https://www.luogu.com.cn/problem/AT_${s.problem_id}`,
timestamp: s.epoch_second * 1000,
isFinal: FINAL_AT.has(s.result)
}));
} catch (e) {
LOG('Kenkoooo 拉取失败', e);
return [];
}
};
const fetchATData = async (uid) => {
const pMap = await fetchATProblems();
const subs = await fetchATSubmissions(pMap, uid);
return subs.map((s) => ({ ...s, taskDisplay: fmtATTask(s.taskKey, pMap) }));
};
let isCFSyncing = false;
let isATSyncing = false;
const syncCF = async (options = {}) => {
if (isCFSyncing) return;
isCFSyncing = true;
try {
const { render = false, renderFn = null, mainDiv = null, force = false } = options;
const cache = getCache(CACHE_CF);
const lastUpdate = GM_getValue(LAST_UPDATE_CF, 0);
const isStale = (Date.now() - lastUpdate) > SYNC_INTERVAL_MS;
if (!force && cache.length > 0 && !isStale) {
if (render && renderFn && mainDiv) renderFn(cache, mainDiv);
return;
}
const handle = GM_getValue('cf-handle', '');
if (!handle) {
LOG('未找到 CF 用户名');
return;
}
const subs = await fetchCFData(handle);
saveCache(CACHE_CF, subs);
markUpdated(LAST_UPDATE_CF);
if (render && renderFn && mainDiv) renderFn(subs, mainDiv);
} catch (e) {
LOG('CF 同步失败', e.message || e);
} finally {
isCFSyncing = false;
}
};
const syncAT = async (options = {}) => {
if (isATSyncing) return;
isATSyncing = true;
try {
const { render = false, renderFn = null, mainDiv = null, force = false } = options;
const cache = getCache(CACHE_AT);
const lastUpdate = GM_getValue(LAST_UPDATE_AT, 0);
const isStale = (Date.now() - lastUpdate) > SYNC_INTERVAL_MS;
if (!force && cache.length > 0 && !isStale) {
if (render && renderFn && mainDiv) renderFn(cache, mainDiv);
return;
}
const atHandle = GM_getValue('at-handle', '');
if (!atHandle) {
LOG('未找到 AT 用户名');
return;
}
const subs = await fetchATData(atHandle);
const mm = new Map();
for (const s of [...subs, ...cache]) {
if (!mm.has(s.id)) mm.set(s.id, s);
}
const merged = Array.from(mm.values()).sort((a, b) => Number(b.id) - Number(a.id));
saveCache(CACHE_AT, merged);
markUpdated(LAST_UPDATE_AT);
if (render && renderFn && mainDiv) renderFn(merged, mainDiv);
} catch (e) {
LOG('AT 同步失败', e.message || e);
} finally {
isATSyncing = false;
}
};
const startBackgroundSync = () => {
if (syncStarted) return;
syncStarted = true;
let disableSync = GM_getValue('rmj-disable-sync', false) === true;
try {
const params = new URLSearchParams(location.search);
if (params.get('rmj-disable-sync') === 'true') disableSync = true;
} catch { /* ignore */ }
if (!disableSync) {
syncCF({ render: false });
syncAT({ render: false });
} else {
LOG('同步已禁用,仅显示模式');
}
};
const destroy = () => {
pageGen++;
timers.clearAll();
pageKey = '';
};
const init = async () => {
if (!isLuogu()) return;
startBackgroundSync();
const URL_PATH = location.pathname;
if (!/^\/record\/list/.test(URL_PATH)) {
if (pageKey) destroy();
return;
}
const params = new URLSearchParams(window.location.search);
const rmj = params.get('rmj');
if (!['1', '2'].includes(rmj)) {
if (pageKey) destroy();
return;
}
const key = `${URL_PATH}?rmj=${rmj}`;
if (pageKey === key) return;
destroy();
pageKey = key;
const myGen = pageGen;
const pid = params.get('pid') || '';
const contest = params.get('contest') || '';
const isCFMode = rmj === '1';
const isATMode = rmj === '2';
const disableSync = GM_getValue('rmj-disable-sync', false) === true ||
params.get('rmj-disable-sync') === 'true';
const STATUS_MAP = isCFMode ? {
OK: ['Accepted', 'cf-status-accepted'], WRONG_ANSWER: ['Wrong Answer', 'cf-status-wrong'],
COMPILATION_ERROR: ['Compilation Error', 'cf-status-ce'], RUNTIME_ERROR: ['Runtime Error', 'cf-status-re'],
TIME_LIMIT_EXCEEDED: ['Time Limit Exceeded', 'cf-status-limit'], MEMORY_LIMIT_EXCEEDED: ['Memory Limit Exceeded', 'cf-status-limit'],
OUTPUT_LIMIT_EXCEEDED: ['Output Limit Exceeded', 'cf-status-limit'], TESTING: ['Running', 'cf-status-running']
} : {
AC: ['Accepted', 'cf-status-accepted'], WA: ['Wrong Answer', 'cf-status-wrong'],
CE: ['Compilation Error', 'cf-status-ce'], RE: ['Runtime Error', 'cf-status-re'],
TLE: ['Time Limit Exceeded', 'cf-status-limit'], MLE: ['Memory Limit Exceeded', 'cf-status-limit'],
OLE: ['Output Limit Exceeded', 'cf-status-limit'], IE: ['Internal Error', 'cf-status-ce']
};
const style = document.createElement('style');
style.textContent = `
.cf-card{display:flex;align-items:center;font-size:16px;padding:10px 0;border-bottom:1px solid #eee}
.cf-avatar img{width:35px;height:35px;border-radius:50%;margin-right:12px}
.cf-nameblock{display:flex;flex-direction:column;width:180px;margin-right:12px}
.cf-name{font-weight:bold;color:#333;font-size:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cf-time{font-size:14px;color:#888;line-height:1.2;margin-top:2px;white-space:nowrap}
.cf-status{flex:0 0 auto;margin-right:12px;text-align:left;width:200px}
.cf-status span{border-radius:2px;padding:2px 3px;font-size:14px;font-weight:600;white-space:nowrap;display:inline-flex;align-items:center;position:relative;max-width:100%;overflow:hidden;text-overflow:ellipsis;box-sizing:border-box}
.cf-status-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cf-status-subtext{position:absolute;top:-2px;right:2px;font-size:10px!important;color:rgba(255,255,255,.8);font-weight:bold;line-height:1;pointer-events:none}
.cf-status-accepted{background:rgb(82,196,26);color:#fff}
.cf-status-wrong{background:rgb(231,76,60);color:#fff}
.cf-status-re{background:rgb(157,61,207);color:#fff}
.cf-status-ce{background:rgb(250,219,20);color:#fff}
.cf-status-limit{background:rgb(5,34,66);color:#fff}
.cf-status-running{background:#888;color:#fff}
.cf-status-other{background:rgb(243,156,17);color:#fff}
.cf-problem{flex:1;color:rgb(52,152,219);font-size:16px;text-align:left;min-width:100px;margin-left:60px}
.cf-error{padding:10px;color:#e74c3c;font-weight:bold}
.cf-loading{padding:10px;color:#708090;font-weight:bold}
`;
document.head.appendChild(style);
const getUser = () => {
const ne = document.querySelector("#app > div.user-nav.rside.hide.nav-scrollbar > div.title > div > span")
|| document.querySelector("#app > div.user-nav.rside.show.nav-scrollbar > div.title > div > span");
const ae = document.querySelector("#app > div.top-bar > div.right > div > span.avatar > a > img");
return {
name: (ne?.textContent || 'User').trim(),
color: ne?.style?.color || '#333',
avatar: (ae?.src || 'https://cdn.luogu.com.cn/upload/usericon/default.png').trim()
};
};
const fmtVerdict = (v) => STATUS_MAP[v] || [
v.split('_').map((w) => w[0] + w.slice(1).toLowerCase()).join(' '),
'cf-status-other'
];
const createCard = (sub) => {
const card = document.createElement('div');
card.className = 'cf-card';
if (sub.id) card.dataset.id = sub.id;
const u = getUser();
const av = document.createElement('div');
av.className = 'cf-avatar';
const img = document.createElement('img');
img.src = u.avatar;
img.onerror = () => img.src = 'https://cdn.luogu.com.cn/upload/usericon/1.png';
av.appendChild(img);
const nb = document.createElement('div');
nb.className = 'cf-nameblock';
const nd = document.createElement('div');
nd.className = 'cf-name';
nd.style.color = u.color;
nd.textContent = u.name;
const td = document.createElement('div');
td.className = 'cf-time';
td.textContent = sub.timeText;
nb.append(nd, td);
const sd = document.createElement('div');
sd.className = 'cf-status';
const sl = document.createElement('a');
sl.href = sub.submitUrl;
sl.target = '_blank';
sl.rel = 'noopener noreferrer';
const sp = document.createElement('span');
const [full, cls] = (isCFMode || sub.isFinal) ? fmtVerdict(sub.status) : ['Running', 'cf-status-running'];
sp.className = cls;
const ts = document.createElement('span');
ts.className = 'cf-status-text';
ts.textContent = full;
sp.appendChild(ts);
if (isCFMode && sub.passedTestCount != null && !isNaN(sub.passedTestCount)) {
const st = document.createElement('span');
st.className = 'cf-status-subtext';
st.textContent = String((full === 'AC' || full === 'Accepted')
? sub.passedTestCount
: sub.passedTestCount + 1);
sp.appendChild(st);
}
sl.appendChild(sp);
sd.appendChild(sl);
const pd = document.createElement('div');
pd.className = 'cf-problem';
const pl = document.createElement('a');
pl.href = sub.luoguUrl;
pl.textContent = sub.taskDisplay;
pl.target = '_blank';
pl.rel = 'noopener noreferrer';
pd.appendChild(pl);
card.append(av, nb, sd, pd);
return card;
};
const CACHE_KEY = isCFMode ? CACHE_CF : CACHE_AT;
const getLocalCache = () => getCache(CACHE_KEY);
const saveLocalCache = (c) => saveCache(CACHE_KEY, c);
const updCount = (n) => {
const s = document.querySelector('.number');
if (s) s.textContent = String(n || 0);
};
let searchFilter = pid;
const render = (cache, mainDiv) => {
if (myGen !== pageGen) return;
let filtered = cache;
if (searchFilter) {
const q = isCFMode
? searchFilter.toLowerCase()
: searchFilter.replace(/^AT_/i, '').toLowerCase();
filtered = cache.filter((s) => s.taskDisplay?.toLowerCase().includes(q));
}
mainDiv.innerHTML = '';
updCount(filtered.length);
if (!filtered.length) {
const msg = document.createElement('div');
msg.className = 'cf-loading';
msg.textContent = '';
mainDiv.appendChild(msg);
} else {
filtered.forEach((s) => {
mainDiv.appendChild(createCard({
...s,
timeText: new Date(s.timestamp || 0).toLocaleString('zh-CN')
}));
});
}
};
const bindSearch = async (mainDiv) => {
try {
const input = await waitForEl(
'#app > div.main-container.lside-nav > div > main > div > div:nth-child(1) > section:nth-child(1) > div > input:nth-child(1)',
8000
);
if (myGen !== pageGen) return;
input.value = searchFilter;
const orig = input.oninput;
input.oninput = (e) => {
searchFilter = e.target.value.trim();
const url = new URL(window.location);
if (searchFilter) url.searchParams.set('pid', searchFilter);
else url.searchParams.delete('pid');
history.replaceState(null, '', url);
render(getLocalCache(), mainDiv);
orig?.call(input, e);
};
} catch { LOG('未找到搜索框'); }
};
let mainDiv;
try {
mainDiv = await waitForEl(
'#app > div.main-container.lside-nav > div > main > div > div:nth-child(2)',
10000
);
} catch (e) {
LOG('记录列表容器未找到', e);
return;
}
if (myGen !== pageGen) return;
render(getLocalCache(), mainDiv);
await bindSearch(mainDiv);
if (myGen !== pageGen) return;
if (isCFMode) {
if (!disableSync) {
let isFetching = false;
const updateNow = async () => {
if (myGen !== pageGen) return;
if (isFetching) return;
isFetching = true;
try {
const handle = GM_getValue('cf-handle', '');
if (!handle) return;
const sr = await req(`https://codeforces.com/api/user.status?handle=${encodeURIComponent(handle)}&from=1`);
if (myGen !== pageGen) return;
if (sr.status !== 200) return;
const d = JSON.parse(sr.responseText);
if (d.status !== 'OK') return;
const seen = new Set();
const subs = (d.result || []).filter((s) => {
if (seen.has(s.id)) return false;
seen.add(s.id);
return true;
}).map((s) => ({
id: String(s.id),
taskDisplay: `CF${s.contestId}${s.problem.index} - ${s.problem.name}`,
status: s.verdict || 'Waiting',
timestamp: s.creationTimeSeconds * 1000,
submitUrl: `https://www.luogu.com.cn/record/cf/${s.contestId}/${s.id}`,
luoguUrl: `https://www.luogu.com.cn/problem/CF${s.contestId}${s.problem.index}`,
passedTestCount: s.passedTestCount
}));
if (myGen !== pageGen) return;
saveLocalCache(subs);
markUpdated(LAST_UPDATE_CF);
render(subs, mainDiv);
} catch (e) {
LOG('CF 列表更新失败', e.message || e);
} finally {
isFetching = false;
}
};
updateNow();
timers.iv(updateNow, 6000);
}
} else {
let cache = getLocalCache();
if (!disableSync) {
(async () => {
try {
const atHandle = GM_getValue('at-handle', '');
if (!atHandle) return;
const pMap = await fetchATProblems();
const kSubs = await fetchATSubmissions(pMap, atHandle);
if (myGen !== pageGen) return;
const enhanced = kSubs.map((s) => ({ ...s, taskDisplay: fmtATTask(s.taskKey, pMap) }));
const mm = new Map();
for (const s of [...enhanced, ...cache]) {
if (!mm.has(s.id)) mm.set(s.id, s);
}
cache = Array.from(mm.values()).sort((a, b) => Number(b.id) - Number(a.id));
saveLocalCache(cache);
markUpdated(LAST_UPDATE_AT);
render(cache, mainDiv);
} catch (e) {
LOG('AT 后台加载失败', e);
}
})();
if (contest) {
timers.iv(async () => {
if (myGen !== pageGen) return;
const prev = getLocalCache();
const updated = await crawlATContest(contest, prev);
if (myGen !== pageGen) return;
if (updated !== prev) {
cache = updated;
saveLocalCache(cache);
markUpdated(LAST_UPDATE_AT);
render(cache, mainDiv);
}
}, 6000);
}
}
}
};
return { init, destroy };
})();
// CF/AT 盾提交
const ShieldSubmit = (() => {
const VALID = [
/^https:\/\/www\.luogu\.com\.cn\/problem\/(AT|CF).+/,
/^https:\/\/codeforces\.com\/problemset\/status\?my=on$/,
/^https:\/\/atcoder\.jp\/contests\/[^/]+\/submit\?RMJ=1$/,
/^https:\/\/atcoder\.jp\/contests\/[^/]+\/submissions\/me$/,
/^https:\/\/codeforces\.com\/problemset\/submit\/.+/,
];
const KEYS = ['code', 'contest', 'task', 'back', 'platform', 'pid', 'cf_contest', 'cf_index',
'body_back', 'bg_image', 'bg_b64', 'bg_color', 'bg_repeat', 'bg_size', 'bg_pos',
'mid_mask', 'color_filter', 'card_bg', 'card_blur'];
const P = 'gen-at-';
const set = (k, v) => GM_setValue(P + k, String(v));
const get = (k) => GM_getValue(P + k, '');
const clr = () => KEYS.forEach((k) => GM_setValue(P + k, ''));
const LIMIT = 4e6;
const LOGO = 'https://cdn.luogu.com.cn/upload/image_hosting/yy27itbl.png';
const setFlag = (k, v) => GM_setValue('rmj-flag-' + k, String(v));
const getFlag = (k, def) => GM_getValue('rmj-flag-' + k, def);
const clearFlags = () => GM_setValue('rmj-flag-action', '');
let submitting = false;
let luoguSetupDone = false;
let cfAtSetupDone = false;
const stripUrl = (v) => {
const m = (v || '').match(/url\(\s*(['"]?)(.+?)\1\s*\)/i);
return m ? m[2] : v;
};
const showRedirectMask = () => {
if (document.getElementById('rmj-redirect-mask')) return;
const m = document.createElement('div');
m.id = 'rmj-redirect-mask';
m.style.cssText = 'position:fixed!important;inset:0!important;z-index:2147483647!important;background:rgb(239,239,239)!important;pointer-events:all!important;';
const attach = () => (document.body || document.documentElement).appendChild(m);
if (document.body) attach();
else new MutationObserver((_, o) => { if (document.body) { o.disconnect(); attach(); } })
.observe(document.documentElement, { childList: true });
};
GM_addStyle(`
.swal2-loader{visibility:hidden}
#rmj-overlay{
position:fixed!important;inset:0!important;z-index:999998!important;
display:flex!important;align-items:center!important;justify-content:center!important;
overflow:hidden!important;pointer-events:all!important;cursor:default!important;
background:var(--rmj-back,#f5f5f5)!important;
color:#333!important;box-sizing:border-box!important;-webkit-font-smoothing:antialiased!important;
}
#rmj-overlay *{box-sizing:border-box!important;margin:0!important;padding:0!important;font-family:inherit!important;line-height:inherit!important}
#rmj-overlay .ly{position:absolute!important;inset:0!important;pointer-events:none!important;
-webkit-mask:var(--rmj-mask,none) no-repeat/100% 100%!important;mask:var(--rmj-mask,none) no-repeat/100% 100%!important}
#rmj-overlay .ly-c{background:var(--rmj-color,transparent)!important;filter:var(--rmj-filter,none)!important}
#rmj-overlay .ly-i{background:var(--rmj-img,none) var(--rmj-pos,center)/var(--rmj-size,cover) var(--rmj-repeat,no-repeat)!important}
#rmj-card{
position:relative!important;z-index:1!important;width:420px!important;max-width:90vw!important;
padding:36px 32px 40px!important;border-radius:4px!important;border:none!important;
background:var(--rmj-card,rgba(255,255,255,.78))!important;
backdrop-filter:var(--rmj-blur,blur(14px))!important;-webkit-backdrop-filter:var(--rmj-blur,blur(14px))!important;
box-shadow:0 8px 32px rgba(0,0,0,.18),0 1.5px 6px rgba(0,0,0,.08)!important;
display:flex!important;flex-direction:column!important;align-items:center!important;gap:20px!important;
}
#rmj-overlay.anim #rmj-card{opacity:0!important;transform:translateY(8px)!important;transition:opacity .45s ease .15s,transform .45s ease .15s!important}
#rmj-overlay.anim.open #rmj-card{opacity:1!important;transform:translateY(0)!important}
#rmj-card h2{font-size:20px!important;font-weight:600!important;color:#333!important;letter-spacing:.5px!important;user-select:none!important}
#rmj-card p{font-size:13px!important;color:#888!important;margin-top:-10px!important;text-align:center!important;user-select:none!important}
#rmj-slot{width:100%!important;min-height:78px!important;display:flex!important;align-items:center!important;justify-content:center!important}
#rmj-slot:empty::after{content:'等待验证组件加载…';font-size:13px;color:#aaa}
#rmj-spin{width:26px;height:26px;border:3px solid rgba(0,0,0,.1);border-top-color:#005c97;border-radius:50%;animation:sp .7s linear infinite}
#rmj-error{color:#d32f2f!important;font-size:13px!important;text-align:center!important;word-break:break-all!important;max-height:120px!important;overflow-y:auto!important;padding:8px 12px!important;background:rgba(211,47,47,.08)!important;border-radius:4px!important;width:100%!important;display:none!important}
@keyframes sp{to{transform:rotate(360deg)}}
body.rmj-lock{overflow:hidden!important;height:100vh!important}
`);
const toB64 = (raw) => new Promise((res, rej) => {
const url = stripUrl(raw);
if (!/^https?:\/\//i.test(url || '')) return res('');
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
try {
const cv = document.createElement('canvas');
const MAX = 1920;
let w = img.naturalWidth, h = img.naturalHeight;
if (Math.max(w, h) > MAX) {
const s = MAX / Math.max(w, h);
w = w * s | 0;
h = h * s | 0;
}
cv.width = w;
cv.height = h;
cv.getContext('2d').drawImage(img, 0, 0, w, h);
let q = 0.9, b = '';
while (q >= 0.1) {
b = cv.toDataURL('image/jpeg', q);
if (b.length <= LIMIT) break;
q = +(q - 0.1).toFixed(1);
}
if (b.length > LIMIT) {
const c2 = document.createElement('canvas');
c2.width = w >> 1;
c2.height = h >> 1;
c2.getContext('2d').drawImage(cv, 0, 0, c2.width, c2.height);
b = c2.toDataURL('image/jpeg', 0.5);
}
if (b.length > LIMIT) {
const c3 = document.createElement('canvas');
c3.width = w >> 2;
c3.height = h >> 2;
c3.getContext('2d').drawImage(cv, 0, 0, c3.width, c3.height);
b = c3.toDataURL('image/jpeg', 0.4);
}
res(b.length <= LIMIT ? b : '');
} catch (e) { rej(e); }
};
img.onerror = () => rej(new Error('图片加载失败'));
img.src = url;
});
const applyVars = (el) => ThemeStore.applyTo(el);
const overlay = (anim, ox, oy) => {
document.documentElement.style.overflow = 'hidden';
let el = document.getElementById('rmj-overlay');
if (el) return el;
el = document.createElement('div');
el.id = 'rmj-overlay';
el.innerHTML = `
正在加载验证
通过 Cloudflare 安全验证后将继续提交
`;
applyVars(el);
const mount = () => {
document.body.appendChild(el);
if (!anim) return;
ox ??= innerWidth / 2;
oy ??= innerHeight / 2;
const R = Math.hypot(innerWidth, innerHeight) + 60;
el.classList.add('anim');
el.style.clipPath = `circle(0px at ${ox}px ${oy}px)`;
requestAnimationFrame(() => requestAnimationFrame(() => {
el.style.transition = 'clip-path .6s cubic-bezier(.22,1,.36,1)';
el.style.clipPath = `circle(${R}px at ${ox}px ${oy}px)`;
el.classList.add('open');
}));
};
document.body
? mount()
: new MutationObserver((_, o) => { if (document.body) { o.disconnect(); mount(); } })
.observe(document.documentElement, { childList: true });
return el;
};
const showError = (msg) => {
const errEl = document.getElementById('rmj-error');
const slot = document.getElementById('rmj-slot');
const title = document.querySelector('#rmj-card h2');
if (errEl) { errEl.textContent = msg; errEl.style.display = 'block'; }
if (slot) slot.innerHTML = '';
if (title) title.textContent = '操作失败,请检查你的远程 OJ 状态';
ERR(msg);
};
const onBody = (cb) => document.body
? cb()
: new MutationObserver((_, o) => { if (document.body) { o.disconnect(); cb(); } })
.observe(document.documentElement, { childList: true });
const setupMeta = (plat) => {
document.title = plat === 'codeforces' ? 'GenGen RMJ on CF' : 'GenGen RMJ on AT';
const fixIcon = () => {
document.querySelectorAll('link[rel*="icon"]').forEach((l) => l.remove());
const h = document.head;
for (const s of ['16x16', '32x32', '48x48', '192x192']) {
const l = document.createElement('link');
l.rel = 'icon';
l.type = 'image/png';
l.href = LOGO;
l.sizes = s;
h.appendChild(l);
}
const a = document.createElement('link');
a.rel = 'apple-touch-icon';
a.href = LOGO;
a.sizes = '180x180';
h.appendChild(a);
};
const guard = () => new MutationObserver((muts) => {
for (const m of muts) {
for (const n of m.addedNodes) {
if (n.nodeType === 1 && n.tagName === 'LINK' && /icon/i.test(n.rel) && n.href !== LOGO) {
n.remove();
fixIcon();
return;
}
}
}
}).observe(document.head, { childList: true, subtree: true });
const noTrans = () => {
const h = document.documentElement;
h.classList.add('notranslate');
h.setAttribute('translate', 'no');
h.setAttribute('lang', 'zh-CN');
const add = (a) => {
const m = document.createElement('meta');
Object.entries(a).forEach(([k, v]) => m.setAttribute(k, v));
document.head.appendChild(m);
};
add({ name: 'google', content: 'notranslate' });
add({ 'http-equiv': 'Content-Language', content: 'zh-CN' });
add({ name: 'msapplication-config', content: 'none' });
};
const run = () => { fixIcon(); guard(); noTrans(); };
document.head
? run()
: new MutationObserver((_, o) => { if (document.head) { o.disconnect(); run(); } })
.observe(document.documentElement, { childList: true, subtree: true });
};
const SUBMIT_RE = /\/fe\/api\/problem\/submit\/([^/?#]+)/;
const setupLuogu = () => {
if (luoguSetupDone) return;
if (!location.href.includes('luogu.com.cn')) return;
luoguSetupDone = true;
let click = { x: innerWidth / 2, y: innerHeight / 2 };
document.addEventListener('click', (e) => {
click = { x: e.clientX, y: e.clientY };
}, true);
const pidTarget = (raw) => {
const p = String(raw).trim();
let m = p.match(/^CF(\d+)([A-Z]\d*)$/);
if (m) return { plat: 'codeforces', contest: m[1], index: m[2] };
m = p.match(/^AT_([a-z0-9]+)_(.+)$/i);
if (m) return { plat: 'atcoder', contest: m[1], task: `${m[1]}_${m[2]}` };
return null;
};
const atFromDOM = () => {
for (const a of document.querySelectorAll('a[href*="atcoder.jp/contests/"]')) {
const m = a.href.match(/atcoder\.jp\/contests\/([^/]+)\/tasks\/([^/?#]+)/);
if (m) return { plat: 'atcoder', contest: m[1], task: m[2] };
}
return null;
};
const intercept = async (code, pid) => {
pid = String(pid).trim();
let t = pidTarget(pid);
if (!t && /^AT/i.test(pid)) {
await new Promise((r) => document.body
? r()
: new MutationObserver((_, o) => { if (document.body) { o.disconnect(); r(); } })
.observe(document.documentElement, { childList: true }));
for (let i = 0; i < 30 && !t; i++) {
t = atFromDOM();
if (!t) await sleep(200);
}
}
if (!t) return;
// 未绑定:直接拒绝
const bindKey = t.plat === 'codeforces' ? 'cf-handle' : 'at-handle';
const bindName = t.plat === 'codeforces' ? 'Codeforces' : 'AtCoder';
if (!GM_getValue(bindKey, '')) {
overlay(false);
showError(`请先在 GenGen RMJ 面板中绑定 ${bindName} 账号`);
return;
}
if (t.plat === 'codeforces' && (!/^\d+$/.test(t.contest) || !/^[A-Z]\d*$/.test(t.index))) {
ERR(`CF 目标非法: ${t.contest}/${t.index}`);
return;
}
ThemeStore.extractFromLuogu();
set('code', code + `\n\n// 本代码使用 GenGen RMJ 于 ${Date()} 提交 | ${pid}`);
set('back', location.href);
set('platform', t.plat);
set('pid', pid);
if (t.plat === 'codeforces') {
set('cf_contest', t.contest);
set('cf_index', t.index);
} else {
set('contest', t.contest);
set('task', t.task);
}
// 只在成功时覆盖 bg_b64,避免清空旧数据
const bgRaw = get('bg_image');
if (bgRaw) {
try {
const b64 = await toB64(bgRaw);
if (b64) set('bg_b64', b64);
} catch { /* 保留旧值 */ }
}
overlay(true, click.x, click.y);
const url = t.plat === 'codeforces'
? `https://codeforces.com/problemset/submit/${t.contest}/${t.index}?RMJ=1`
: `https://atcoder.jp/contests/${t.contest}/submit?RMJ=1`;
setTimeout(() => { location.href = url; }, 700);
};
const XHR = unsafeWindow.XMLHttpRequest;
const _open = XHR.prototype.open;
const _send = XHR.prototype.send;
XHR.prototype.open = function (m, u, ...r) {
this._rmj = { m: m.toUpperCase(), u: String(u) };
return _open.call(this, m, u, ...r);
};
XHR.prototype.send = function (body) {
const m = this._rmj?.m === 'POST' && SUBMIT_RE.exec(this._rmj.u);
if (m) {
let code = '';
try { code = JSON.parse(String(body ?? '')).code || ''; } catch { /* ignore */ }
if (code.trim()) intercept(code, m[1]);
this.abort();
return;
}
return _send.call(this, body);
};
};
const setupCfAt = () => {
if (cfAtSetupDone) return;
cfAtSetupDone = true;
const h = location.href;
const isFromRMJ = document.referrer.includes('RMJ=1') || getFlag('action');
if (h.includes('codeforces.com/problemset/status') && isFromRMJ) {
showRedirectMask();
handleStatusPage('codeforces');
return;
}
if (h.includes('atcoder.jp/contests/') && h.includes('/submissions/me') && isFromRMJ) {
showRedirectMask();
handleStatusPage('atcoder');
return;
}
onBody(async () => {
if (h.includes('atcoder.jp') && h.includes('RMJ=1')) init('atcoder');
else if (h.includes('codeforces.com') && new URL(h).searchParams.get('RMJ') === '1') init('codeforces');
});
};
function init(plat) {
setupMeta(plat);
document.documentElement.classList.add('rmj-lock');
overlay(false);
const go = () => submit(plat);
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', go)
: go();
}
async function handleStatusPage(plat) {
let sid = null;
for (let i = 0; i < 10; i++) {
sid = await archiveCode(plat);
if (sid) break;
await sleep(1500);
}
clearFlags();
let targetUrl;
if (sid) {
if (plat === 'codeforces') {
targetUrl = `https://www.luogu.com.cn/record/cf/${get('cf_contest')}/${sid}`;
} else {
targetUrl = `https://www.luogu.com.cn/record/at/${get('contest')}/${sid}`;
}
} else {
if (plat === 'codeforces') {
targetUrl = `https://codeforces.com/contest/${get('cf_contest')}/my`;
} else {
targetUrl = `https://atcoder.jp/contests/${get('contest')}/submissions/me`;
}
}
location.href = targetUrl;
}
async function archiveCode(plat) {
try {
const c = plat === 'codeforces' ? get('cf_contest') : get('contest');
if (plat === 'codeforces') {
const code = get('code');
if (!code) return;
const my = `https://codeforces.com/contest/${c}/my`;
const doc = new DOMParser().parseFromString(
await (await fetch(my, { credentials: 'include' })).text(),
'text/html'
);
const sid = (doc.querySelector('a[href*="/submission"]')?.href || '')
.match(/submissions?\/(\d+)/)?.[1];
if (sid) GM_setValue(`{${c},${sid}}`, code);
return sid;
} else {
const res = await fetch(
`https://atcoder.jp/contests/${c}/submissions/me/status/json`,
{ credentials: 'include' }
);
if (!res.ok) return null;
const data = await res.json();
if (data && data.Result) {
const ids = Object.keys(data.Result);
if (ids.length > 0) {
ids.sort((a, b) => Number(b) - Number(a));
return ids[0];
}
}
return null;
}
} catch (e) {
ERR(e);
}
}
async function submit(plat) {
if (submitting) return;
submitting = true;
try {
const code = get('code');
if (!code?.trim()) {
showError('代码为空,请返回重新操作');
clr();
return;
}
document.documentElement.classList.remove('rmj-lock');
let form = null;
for (let i = 0; i < 100; i++) {
form = plat === 'atcoder'
? document.querySelector('form[action*="/submit"]')
: document.querySelector('form.submitFrameForm')
|| document.querySelector('form.submit-form')
|| document.querySelector('form[method="post"][action*="csrf_token"]');
if (form) break;
await sleep(100);
}
if (!form) {
const blocked = /403|just a moment|attention required/i.test(document.title)
|| !!document.querySelector('#challenge-stage,#challenge-form,.cf-challenge,iframe[src*="challenges.cloudflare.com"]');
if (blocked) {
showError('页面被 Cloudflare 拦截,请手动完成验证后刷新');
return;
}
showError('未找到提交表单');
clr();
return;
}
if (plat === 'atcoder') {
const ta = document.getElementById('plain-textarea');
if (ta) { ta.value = code; ta.dispatchEvent(new Event('input', { bubbles: true })); }
const ace = document.getElementById('editor') || document.querySelector('.ace_editor');
if (ace && unsafeWindow.ace) {
try {
const e = unsafeWindow.ace.edit(ace);
e.setValue(code, 1);
e.clearSelection();
} catch { /* ignore */ }
}
} else {
const ta = document.querySelector('#sourceCodeTextarea');
if (ta) { ta.value = code; ta.dispatchEvent(new Event('input', { bubbles: true })); }
const ace = document.getElementById('editor');
if (ace && unsafeWindow.ace) {
try {
const e = unsafeWindow.ace.edit(ace);
e.setValue(code, 1);
e.clearSelection();
} catch { /* ignore */ }
}
}
if (plat === 'atcoder') {
const task = get('task');
if (task) {
const s = document.querySelector('select[name="data.TaskScreenName"]');
if (s) { s.value = task; s.dispatchEvent(new Event('change', { bubbles: true })); }
}
const l = document.querySelector('select[name="data.LanguageId"]');
if (l) { l.value = '6017'; l.dispatchEvent(new Event('change', { bubbles: true })); }
} else {
const l = document.querySelector('select[name="programTypeId"]');
if (l) { l.value = '91'; l.dispatchEvent(new Event('change', { bubbles: true })); }
}
const CF = '.cf-challenge,.cf-turnstile,iframe[src*="challenges.cloudflare.com"],[data-callback*="turnstile"]';
let hasCF = false;
for (let i = 0; i < 30; i++) {
if (document.querySelector(CF)) { hasCF = true; break; }
await sleep(100);
}
let cfToken = '';
if (hasCF) {
const cfWidget = document.querySelector(CF);
const slot = document.getElementById('rmj-slot');
if (slot) {
const updatePosition = () => {
const rect = slot.getBoundingClientRect();
const w = 300, h = 65;
const top = rect.top + (rect.height - h) / 2;
const left = rect.left + (rect.width - w) / 2;
cfWidget.style.position = 'fixed';
cfWidget.style.top = `${top}px`;
cfWidget.style.left = `${left}px`;
cfWidget.style.width = `${w}px`;
cfWidget.style.height = `${h}px`;
cfWidget.style.zIndex = '9999999';
cfWidget.style.margin = '0';
cfWidget.style.padding = '0';
};
updatePosition();
window.addEventListener('resize', updatePosition);
}
let tokenFound = false;
for (let i = 0; ; i++) {
const t = document.querySelector('input[name="turnstileToken"],input[name^="cf-turnstile-response"]');
if (t?.value?.length > 10) {
cfToken = t.value;
tokenFound = true;
break;
}
await sleep(100);
}
if (!tokenFound) {
showError('验证超时,请手动点击提交');
return;
}
}
if (plat === 'codeforces' && hasCF) {
setFlag('action', 'submit');
let submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
if (!submitBtn) submitBtn = form.querySelector('.submit-form-submit, .submit, [name="action"][value="submitSolutionFormSubmitted"]');
if (submitBtn) {
submitBtn.click();
} else {
HTMLFormElement.prototype.submit.call(form);
}
return;
}
const contest = get('contest');
const cfcon = get('cf_contest');
let url;
if (plat === 'codeforces') {
const csrf = form.querySelector('input[name="csrf_token"]')?.value
|| document.querySelector('meta[name="csrf_token"]')?.content || '';
url = location.origin + location.pathname;
if (csrf) url += (url.includes('?') ? '&' : '?') + 'csrf_token=' + encodeURIComponent(csrf);
} else {
url = form.action || location.href;
}
const fd = new FormData(form);
if (plat === 'codeforces') {
fd.set('programTypeId', '91');
fd.set('source', code);
} else if (plat === 'atcoder') {
fd.set('sourceCode', code);
}
if (cfToken) {
fd.set('cf-turnstile-response', cfToken);
fd.set('turnstileToken', cfToken);
}
try {
const r = await fetch(url, {
method: 'POST',
body: fd,
credentials: 'include',
redirect: 'manual',
headers: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
Referer: location.href
}
});
const isSuccess = r.type === 'opaqueredirect' || r.status === 302 || r.status === 303 || r.ok;
if (!isSuccess) throw new Error(`HTTP ${r.status}`);
let sid = null;
for (let i = 0; i < 8; i++) {
await sleep(1000);
sid = await archiveCode(plat);
if (sid) break;
}
clr();
if (plat === 'codeforces') {
location.href = `https://www.luogu.com.cn/record/cf/${cfcon}/${sid || ''}`;
} else if (plat === 'atcoder') {
location.href = `https://www.luogu.com.cn/record/at/${contest}/${sid || ''}`;
}
} catch (e) {
showError(`提交失败:\n${e.message}`);
clr();
}
} finally {
submitting = false;
}
}
const run = () => {
if (!VALID.some((r) => r.test(location.href))) return;
setupLuogu();
setupCfAt();
};
return { init: run, showRedirectMask };
})();
// 主调度
const bootstrap = () => {
CfChallengePatch.apply();
if (isCF()) {
MainUI.captureCfHandle();
ShieldSubmit.init();
return;
}
if (isLuogu()) {
MainUI.init();
RecordList.init();
RecordDetail.init();
ShieldSubmit.init();
ThemeStore.extractWithRetry();
Router.on(() => {
MainUI.hideModal();
MainUI.handleRoute();
RecordList.init();
RecordDetail.init();
ShieldSubmit.init();
});
return;
}
if (isAT()) {
ShieldSubmit.init();
return;
}
if (isCfChallenge()) return;
};
docReady(bootstrap);
})();