NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Gen-CF-RMJ 提交 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 从洛谷一键提交到 Codeforces,优化UI显示和提交进度 // @match https://www.luogu.com.cn/problem/CF* // @connect codeforces.com // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function () { 'use strict'; if (!window.location.href.startsWith('https://www.luogu.com.cn/problem/CF')) { return; } let currentStep = 0; const steps = [ "正在获取代码...", "正在连接 Codeforces...", "正在获取安全令牌...", "正在提交代码...", "正在验证提交..." ]; let originalButtonState = null; // 持续监听并直接删除 SweetAlert2 弹窗 function initSwalInterceptor() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1 && node.classList && node.classList.contains('swal2-container')) { console.log("[Gen-CF-RMJ] 检测到 SweetAlert2 弹窗,直接删除"); setTimeout(() => node.remove(), 100); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); } function updateProgress(step, message = "") { currentStep = step; const progress = ((step + 1) / steps.length * 100).toFixed(0); const text = message || steps[step]; const progressBar = document.querySelector(".gen-cf-progress") || createProgressUI(); // 只更新变化的部分,避免重新渲染整个UI const progressFill = progressBar.querySelector(".gen-cf-progress-fill"); const progressText = progressBar.querySelector(".gen-cf-progress-text"); const messageContainer = progressBar.querySelector(".gen-cf-message"); if (progressFill) { progressFill.style.width = `${progress}%`; } if (progressText) { progressText.textContent = `${progress}%`; } // 更新步骤状态 steps.forEach((s, i) => { const stepElement = progressBar.querySelector(`.gen-cf-step[data-step="${i}"]`); if (stepElement) { const icon = stepElement.querySelector(".gen-cf-step-icon"); if (i < step) { stepElement.classList.add("active", "completed"); stepElement.classList.remove("current"); if (icon) icon.textContent = "✓"; } else if (i === step) { stepElement.classList.add("active", "current"); stepElement.classList.remove("completed"); if (icon) icon.textContent = (i + 1).toString(); } else { stepElement.classList.remove("active", "current", "completed"); if (icon) icon.textContent = (i + 1).toString(); } } }); // 更新消息 if (message) { if (messageContainer) { messageContainer.textContent = message; messageContainer.style.display = "block"; } else { const newMessage = document.createElement("div"); newMessage.className = "gen-cf-message"; newMessage.textContent = message; progressBar.querySelector(".gen-cf-steps").after(newMessage); } } else if (messageContainer) { messageContainer.style.display = "none"; } } function createProgressUI() { const existing = document.querySelector(".gen-cf-progress"); if (existing) existing.remove(); const progressBar = document.createElement("div"); progressBar.className = "gen-cf-progress"; progressBar.style.opacity = "0"; progressBar.style.transform = "translate(-50%, -50%) scale(0.8)"; // 初始渲染 progressBar.innerHTML = ` <div class="gen-cf-header"> <h3>Gen-CF Submitting...</h3> <div class="gen-cf-progress-text">0%</div> </div> <div class="gen-cf-progress-bar"> <div class="gen-cf-progress-fill" style="width: 0%"></div> </div> <div class="gen-cf-steps"> ${steps.map((s, i) => ` <div class="gen-cf-step" data-step="${i}"> <span class="gen-cf-step-icon">${i + 1}</span> ${s} </div> `).join('')} </div> <div class="gen-cf-message" style="display: none;"></div> `; document.body.appendChild(progressBar); // 添加入场动画 setTimeout(() => { progressBar.style.opacity = "1"; progressBar.style.transform = "translate(-50%, -50%) scale(1)"; }, 10); const style = document.createElement("style"); style.textContent = ` .gen-cf-progress { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 25px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.2); z-index: 10000; min-width: 450px; border: 1px solid #e1e4e8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .gen-cf-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .gen-cf-header h3 { margin: 0; color: #2c3e50; font-size: 18px; font-weight: 600; } .gen-cf-progress-text { background: #3498db; color: white; padding: 6px 14px; border-radius: 20px; font-size: 14px; font-weight: bold; } .gen-cf-progress-bar { height: 8px; background: #ecf0f1; border-radius: 4px; margin: 20px 0; overflow: hidden; } .gen-cf-progress-fill { height: 100%; background: linear-gradient(90deg, #3498db, #2ecc71); border-radius: 4px; transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; } .gen-cf-progress-fill::after { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); animation: genCfShine 2s infinite; } @keyframes genCfShine { 0% { left: -100%; } 100% { left: 100%; } } .gen-cf-steps { margin: 25px 0; } .gen-cf-step { display: flex; align-items: center; padding: 10px 0; color: #95a5a6; font-size: 14px; transition: all 0.3s ease; } .gen-cf-step.active { color: #2c3e50; } .gen-cf-step.current { font-weight: 600; color: #3498db; } .gen-cf-step.completed { color: #27ae60; } .gen-cf-step-icon { width: 24px; height: 24px; border-radius: 50%; background: #ecf0f1; display: flex; align-items: center; justify-content: center; margin-right: 12px; font-size: 12px; font-weight: bold; transition: all 0.3s ease; } .gen-cf-step.active .gen-cf-step-icon { background: #3498db; color: white; } .gen-cf-step.completed .gen-cf-step-icon { background: #27ae60; color: white; animation: genCfBounce 0.5s ease; } @keyframes genCfBounce { 0%, 20%, 50%, 80%, 100% { transform: scale(1); } 40% { transform: scale(1.2); } 60% { transform: scale(1.1); } } .gen-cf-message { background: #f8f9fa; padding: 12px; border-radius: 8px; margin-top: 15px; font-size: 14px; color: #6c757d; border-left: 4px solid #3498db; line-height: 1.4; } .gen-cf-error { background: #fee; color: #c33; padding: 15px; border-radius: 8px; margin-top: 20px; border: 1px solid #fcc; font-size: 14px; line-height: 1.4; } .gen-cf-buttons { display: flex; gap: 10px; margin-top: 20px; } .gen-cf-btn { flex: 1; padding: 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.3s ease; } .gen-cf-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); } .gen-cf-btn:active { transform: translateY(0); } .gen-cf-btn-primary { background: #3498db; color: white; } .gen-cf-btn-primary:hover { background: #2980b9; } .gen-cf-btn-secondary { background: #95a5a6; color: white; } .gen-cf-btn-secondary:hover { background: #7f8c8d; } `; document.head.appendChild(style); return progressBar; } function showSuccess() { updateProgress(4, "✅ 提交成功!正在跳转到提交记录..."); setTimeout(() => { const urlMatch = location.pathname.match(/CF(\d+)([A-Z]\d?)/i); if (!urlMatch) { showError("无法解析 Codeforces 题目ID"); return; } const contestId = urlMatch[1], index = urlMatch[2]; window.location.href = "https://www.luogu.com.cn/record/list?codeforces=1&pid=CF" + contestId + index; }, 150); } function showError(message) { const progressBar = document.querySelector(".gen-cf-progress"); if (progressBar) { // 添加关闭动画 progressBar.style.opacity = "0"; progressBar.style.transform = "translate(-50%, -50%) scale(0.8)"; setTimeout(() => { progressBar.remove(); resetLuoguButton(); }, 300); } // 显示错误提示 const errorMsg = document.createElement("div"); errorMsg.className = "gen-cf-error-msg"; errorMsg.innerHTML = `❌ 提交失败: ${message}`; errorMsg.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #e74c3c; color: white; padding: 12px 20px; border-radius: 6px; z-index: 10001; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-size: 14px; animation: genCfSlideInRight 0.3s ease-out; `; const style = document.createElement("style"); style.textContent = ` @keyframes genCfSlideInRight { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } } `; document.head.appendChild(style); document.body.appendChild(errorMsg); setTimeout(() => { errorMsg.style.opacity = "0"; errorMsg.style.transform = "translateX(100%)"; setTimeout(() => errorMsg.remove(), 300); }, 3000); } function resetLuoguButton() { const btn = document.querySelector("#app > div.main-container > main > div > div > div.main > div > div.body > button"); if (btn) { btn.disabled = false; btn.style.pointerEvents = "auto"; btn.style.opacity = "1"; const loadingSpinner = btn.querySelector('.loading-spinner, .spinner'); if (loadingSpinner) { loadingSpinner.remove(); } console.log("[Gen-CF-RMJ] 已重置洛谷提交按钮状态"); } } function detectLuoguButton() { const btn = document.querySelector("#app > div.main-container > main > div > div > div.main > div > div.body > button"); if (btn && !btn.dataset.genCfBound) { btn.dataset.genCfBound = "1"; originalButtonState = { disabled: btn.disabled, pointerEvents: btn.style.pointerEvents, opacity: btn.style.opacity }; btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); btn.disabled = true; btn.style.pointerEvents = "none"; btn.style.opacity = "0.7"; startSubmit(); }); console.log("[Gen-CF-RMJ] 绑定洛谷提交按钮成功"); } else { setTimeout(detectLuoguButton, 500); } } async function startSubmit() { const urlMatch = location.pathname.match(/CF(\d+)([A-Z]\d?)/i); if (!urlMatch) { showError("无法解析 Codeforces 题目ID"); return; } const contestId = urlMatch[1], index = urlMatch[2]; try { updateProgress(0); const code = await getFullCodeSafe(); console.log("[Gen-CF-RMJ] 获取代码成功,长度:", code.length); updateProgress(1, "正在连接 Codeforces 服务器..."); const submitPage = await fetchCfSubmitPage(contestId, index); updateProgress(2, "正在获取安全令牌..."); const token = extractCsrfToken(submitPage); if (!token) { showError("获取安全令牌失败,请检查登录状态"); return; } console.log("[Gen-CF-RMJ] 获取 CSRF Token 成功"); updateProgress(3, "正在提交代码到 Codeforces..."); await submitCode(contestId, index, code, token); showSuccess(); } catch (err) { console.error("[Gen-CF-RMJ] 提交错误:", err); showError(err.message); } } async function getFullCodeSafe() { const view = await waitForEditorView(); if (!view) throw new Error("无法获取代码编辑器内容"); return view.state.doc.toString(); } function waitForEditorView() { return new Promise((resolve) => { const start = Date.now(); const tick = () => { const editors = document.querySelectorAll(".cm-editor, .cm-content"); for (const ed of editors) { const view = ed.cmView?.view; if (view?.state?.doc) return resolve(view); } if (Date.now() - start < 6000) setTimeout(tick, 100); else resolve(null); }; tick(); }); } function fetchCfSubmitPage(contestId, index) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://codeforces.com/problemset/submit`, onload: function (res) { if (res.status === 200) { console.log("[Gen-CF-RMJ] 成功访问 Codeforces 提交页面"); resolve(res.responseText); } else { reject(new Error(`HTTP ${res.status}: 无法访问 Codeforces 提交页面`)); } }, onerror: () => reject(new Error("网络连接失败,请检查网络或 CF 登录状态")) }); }); } function extractCsrfToken(html) { const match = html.match(/<meta name="X-Csrf-Token" content="([^"]*)"/); return match ? match[1] : null; } function submitCode(contestId, index, code, token) { return new Promise((resolve, reject) => { const formData = new URLSearchParams(); formData.append("csrf_token", token); formData.append("action", "submitSolutionFormSubmitted"); formData.append("contestId", contestId); formData.append("submittedProblemIndex", index); formData.append("programTypeId", "54"); formData.append("source", code); formData.append("tabSize", "4"); formData.append("_tta", "377"); GM_xmlhttpRequest({ method: "POST", url: "https://codeforces.com/problemset/submit?csrf_token=" + encodeURIComponent(token), data: formData.toString(), headers: { "Content-Type": "application/x-www-form-urlencoded", "Origin": "https://codeforces.com", "Referer": `https://codeforces.com/problemset/submit` }, onload: function (res) { console.log("[Gen-CF-RMJ] 提交响应状态:", res.status); console.log("[Gen-CF-RMJ] 提交响应URL:", res.finalUrl); if (res.status === 200) { // 检查是否提交成功 if (res.finalUrl && res.finalUrl.includes("/problemset/status")) { resolve(); } else if (res.responseText.includes("submitted successfully")) { resolve(); } else { // 修复:更好的错误信息解析 let errorMessage = "提交失败:未知错误"; // 尝试多种方式解析错误信息 const errorMatch1 = res.responseText.match(/error.*?>(.*?)</i); const errorMatch2 = res.responseText.match(/class=["']error["'][^>]*>([^<]+)/i); const errorMatch3 = res.responseText.match(/<span[^>]*class=["']*error["']*[^>]*>([^<]+)<\/span>/i); if (errorMatch1 && errorMatch1[1].trim() && errorMatch1[1].trim() !== " ") { errorMessage = errorMatch1[1].trim(); } else if (errorMatch2 && errorMatch2[1].trim() && errorMatch2[1].trim() !== " ") { errorMessage = errorMatch2[1].trim(); } else if (errorMatch3 && errorMatch3[1].trim() && errorMatch3[1].trim() !== " ") { errorMessage = errorMatch3[1].trim(); } else if (res.responseText.includes("You have submitted exactly the same code before")) { errorMessage = "您已经提交过完全相同的代码"; } else if (res.responseText.includes("You should be authorized")) { errorMessage = "请先登录 Codeforces"; } else if (res.responseText.includes("The problem is not available")) { errorMessage = "题目不可用或不存在"; } else if (res.responseText.includes("Source code length is limited")) { errorMessage = "代码长度超过限制"; } // 如果还是无法解析到具体错误,使用通用错误信息 if (errorMessage === "提交失败:未知错误") { errorMessage = "提交失败:请检查登录状态、网络连接和代码内容"; } reject(new Error(errorMessage)); } } else { reject(new Error(`HTTP ${res.status}: 提交请求失败`)); } }, onerror: (err) => { reject(new Error("网络请求失败,请检查网络连接")); } }); }); } // 初始化 console.log("[Gen-CF-RMJ] 脚本加载成功"); detectLuoguButton(); initSwalInterceptor(); })();