liyifan202201outlook.com / Gen-CF-RMJ 提交

// ==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() !== "&nbsp;") {
                errorMessage = errorMatch1[1].trim();
              }
              else if (errorMatch2 && errorMatch2[1].trim() && errorMatch2[1].trim() !== "&nbsp;") {
                errorMessage = errorMatch2[1].trim();
              }
              else if (errorMatch3 && errorMatch3[1].trim() && errorMatch3[1].trim() !== "&nbsp;") {
                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();
})();