ChatGPT Degraded

Monitor ChatGPT service level, IP quality and PoW difficulty

目前為 2025-02-10 提交的版本,檢視 最新版本

// ==UserScript==
// @name         ChatGPT Degraded
// @name:zh-CN   ChatGPT 服务降级监控
// @namespace    https://github.com/lroolle/chatgpt-degraded
// @version      0.2.1
// @description  Monitor ChatGPT service level, IP quality and PoW difficulty
// @description:zh-CN  监控 ChatGPT 服务状态、IP 质量和 PoW 难度
// @author       lroolle
// @license      AGPL-3.0
// @match        *://chatgpt.com/*
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojMmE5ZDhmO3N0b3Atb3BhY2l0eToxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3R5bGU9InN0b3AtY29sb3I6IzJhOWQ4ZjtzdG9wLW9wYWNpdHk6MC44Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Zz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjI4IiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KPCEtLU91dGVyIGNpcmNsZSBtb2RpZmllZCB0byBsb29rIGxpa2UgIkMiLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEyNSA1NSIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjIwIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjQiIGZpbGw9IiNmZmYiLz4KICA8L2c+Cjwvc3ZnPg==
// @grant        none
// @homepageURL  https://github.com/lroolle/chatgpt-degraded
// @supportURL   https://github.com/lroolle/chatgpt-degraded/issues
// ==/UserScript==

// Reference: https://github.com/KoriIku/chatgpt-degrade-checker (by KoriIku)

(function () {
  "use strict";

  const displayBox = document.createElement("div");
  displayBox.style.position = "fixed";
  displayBox.style.bottom = "10px";
  displayBox.style.right = "80px";
  displayBox.style.transform = "none";
  displayBox.style.width = "320px";
  displayBox.style.padding = "24px";
  displayBox.style.backgroundColor =
    "var(--surface-primary, rgb(255, 255, 255))";
  displayBox.style.color = "var(--text-primary, #374151)";
  displayBox.style.fontSize = "14px";
  displayBox.style.borderRadius = "16px";
  displayBox.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
  displayBox.style.zIndex = "10000";
  displayBox.style.transition = "opacity 0.15s ease, transform 0.15s ease";
  displayBox.style.display = "none";
  displayBox.style.opacity = "0";
  displayBox.style.transform = "translateX(10px)";
  displayBox.style.border =
    "1px solid var(--border-light, rgba(0, 0, 0, 0.05))";

  displayBox.innerHTML = `
        <div id="content">
            <div class="monitor-item">
                <div class="monitor-row">
                    <span class="label" title="ChatGPT Account Type">ChatGPT</span>
                    <span id="user-type" class="value"></span>
                </div>
            </div>
            <div class="monitor-item">
                <div class="monitor-row">
                    <span class="label" title="Your IP Address">IP</span>
                    <div class="ip-container">
                        <span id="ip-address" class="value monospace" title="Click to copy"></span>
                        <span id="warp-badge" class="warp-badge" title=""></span>
                        <span id="ip-quality" class="value-tag"></span>
                    </div>
                </div>
                <div class="progress-wrapper" data-tooltip="Risk Level: Indicates the risk associated with your connection. Lower is better, higher may indicate VPN/proxy detection.">
                    <div class="progress-container">
                        <div id="ip-quality-bar" class="progress-bar"></div>
                    </div>
                    <div class="progress-background"></div>
                </div>
            </div>
            <div class="monitor-item">
                <div class="monitor-row">
                    <span class="label" title="Proof of Work Difficulty">PoW</span>
                    <div class="pow-container">
                        <span id="difficulty" class="value monospace"></span>
                        <span id="pow-level" class="value-tag"></span>
                    </div>
                </div>
                <div class="progress-wrapper" data-tooltip="PoW Difficulty: Required computational work before sending messages. Lower (green) means faster responses.">
                    <div class="progress-container">
                        <div id="pow-bar" class="progress-bar"></div>
                    </div>
                    <div class="progress-background"></div>
                </div>
            </div>
            <div class="monitor-item">
                <div class="monitor-row">
                    <span class="label" title="OpenAI System Status">Status</span>
                    <a id="status-description" href="https://status.openai.com" target="_blank" class="value">
                        Checking status...
                    </a>
                </div>
            </div>
        </div>
        <style>
            .monitor-item {
                margin-bottom: 16px;
            }
            .monitor-item:last-child {
                margin-bottom: 0;
            }
            .monitor-row {
                display: flex;
                align-items: center;
                gap: 6px;
                margin-bottom: 6px;
            }
            .label {
                font-size: 14px;
                color: var(--text-secondary, #6B7280);
                flex-shrink: 0;
                min-width: 40px;
                font-weight: 400;
            }
            .value {
                font-size: 14px;
                color: var(--text-primary, #374151);
                flex: 1;
            }
            .monospace {
                font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
                font-size: 14px;
            }
            .value-tag {
                font-size: 14px;
                color: var(--success-color, #10a37f);
                white-space: nowrap;
                font-weight: 500;
            }
            .progress-wrapper {
                position: relative;
                margin-left: 40px;
            }
            .progress-container {
                position: relative;
                height: 4px;
                background: transparent;
                border-radius: 2px;
                overflow: hidden;
                z-index: 1;
            }
            .progress-background {
                position: absolute;
                top: 0;
                left: 0;
                right: 0;
                height: 4px;
                background: var(--surface-secondary, rgba(0, 0, 0, 0.08));
                border-radius: 2px;
            }
            .progress-bar {
                height: 100%;
                width: 0%;
                transition: all 0.3s ease;
                background: var(--success-color, #10a37f);
            }
            #status-description {
                text-decoration: none;
                color: inherit;
            }
            #status-description:hover {
                text-decoration: underline;
            }
            #ip-address {
                cursor: pointer;
            }
            #ip-address:hover {
                opacity: 0.7;
            }
            #user-type {
                font-weight: 500;
            }
            .ip-container, .pow-container {
                display: flex;
                align-items: center;
                gap: 6px;
                flex: 1;
            }
            .warp-badge {
                font-size: 12px;
                color: var(--success-color, #10a37f);
                background-color: var(--surface-secondary, rgba(16, 163, 127, 0.1));
                padding: 2px 4px;
                border-radius: 4px;
                font-weight: 500;
                cursor: help;
                display: none;
            }
        </style>`;
  document.body.appendChild(displayBox);

  const collapsedIndicator = document.createElement("div");
  collapsedIndicator.style.position = "fixed";
  collapsedIndicator.style.bottom = "10px";
  collapsedIndicator.style.right = "40px";
  collapsedIndicator.style.transform = "none";
  collapsedIndicator.style.width = "24px";
  collapsedIndicator.style.height = "24px";
  collapsedIndicator.style.backgroundColor = "transparent";
  collapsedIndicator.style.border =
    "1px solid var(--token-border-light, rgba(0, 0, 0, 0.1))";
  collapsedIndicator.style.borderRadius = "50%";
  collapsedIndicator.style.cursor = "pointer";
  collapsedIndicator.style.zIndex = "10000";
  collapsedIndicator.style.display = "flex";
  collapsedIndicator.style.alignItems = "center";
  collapsedIndicator.style.justifyContent = "center";
  collapsedIndicator.style.transition = "all 0.3s ease";

  collapsedIndicator.innerHTML = `
    <svg width="24" height="24" viewBox="0 0 64 64">
        <defs>
            <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
                <stop offset="0%" style="stop-color:var(--token-text-secondary, #666);stop-opacity:1" />
                <stop offset="100%" style="stop-color:var(--token-text-secondary, #666);stop-opacity:0.8" />
            </linearGradient>
            <filter id="glow">
                <feGaussianBlur stdDeviation="1" result="coloredBlur"/>
                <feMerge>
                    <feMergeNode in="coloredBlur"/>
                    <feMergeNode in="SourceGraphic"/>
                </feMerge>
            </filter>
        </defs>
        <g id="icon-group" filter="url(#glow)" transform="rotate(165, 32, 32)">
            <circle cx="32" cy="32" r="28" fill="url(#gradient)" stroke="#fff" stroke-width="1"/>
            <circle cx="32" cy="32" r="20" fill="none" stroke="#fff" stroke-width="1"
                    stroke-dasharray="80 40" transform="rotate(-90, 32, 32)">
                <animate
                    attributeName="r"
                    values="20;22;20"
                    dur="4s"
                    repeatCount="indefinite"/>
            </circle>
            <circle cx="32" cy="32" r="12" fill="none" stroke="#fff" stroke-width="1">
                <animate
                    attributeName="r"
                    values="12;14;12"
                    dur="4s"
                    repeatCount="indefinite"/>
            </circle>
            <circle id="center-dot" cx="32" cy="32" r="4" fill="#fff">
                <animate
                    attributeName="r"
                    values="4;6;4"
                    dur="4s"
                    repeatCount="indefinite"/>
            </circle>
        </g>
    </svg>`;
  document.body.appendChild(collapsedIndicator);

  collapsedIndicator.addEventListener("mouseenter", () => {
    displayBox.style.display = "block";
    requestAnimationFrame(() => {
      displayBox.style.opacity = "1";
      displayBox.style.transform = "translateX(0)";
    });
  });

  displayBox.addEventListener("mouseleave", () => {
    displayBox.style.opacity = "0";
    displayBox.style.transform = "translateX(10px)";
    setTimeout(() => {
      displayBox.style.display = "none";
    }, 150);
  });

  // Add IP masking function
  function maskIP(ip) {
    if (!ip || ip === "Unknown") return ip;

    // For IPv4: 192.168.1.1 -> 192.*.*.1
    if (ip.includes(".")) {
      const parts = ip.split(".");
      return `${parts[0]}.*.*.${parts[3]}`;
    }

    // For IPv6: 2a09:bac5:6248:183c::26a:1a -> 2a09:*:*:183c
    if (ip.includes(":")) {
      const parts = ip.split(":");
      return `${parts[0]}:*:*:${parts[3]}`;
    }

    return ip;
  }

  async function fetchIPInfo() {
    try {
      const response = await fetch("https://chatgpt.com/cdn-cgi/trace");
      const text = await response.text();
      const data = text.split("\n").reduce((obj, line) => {
        const [key, value] = line.split("=");
        if (key && value) obj[key.trim()] = value.trim();
        return obj;
      }, {});

      const ipElement = document.getElementById("ip-address");
      const warpBadge = document.getElementById("warp-badge");
      const maskedIP = maskIP(data.ip);
      const fullIP = data.ip || "Unknown";
      const warpStatus = data.warp || "off";

      ipElement.innerText = maskedIP;

      // Update WARP badge visibility and content
      if (warpStatus === "on") {
        warpBadge.style.display = "inline-flex";
        warpBadge.innerText = "warp";
        warpBadge.title = "Protected by Cloudflare WARP";
      } else if (warpStatus === "plus") {
        warpBadge.style.display = "inline-flex";
        warpBadge.innerText = "warp+";
        warpBadge.title = "Protected by Cloudflare WARP+";
      } else {
        warpBadge.style.display = "none";
      }

      // Add tooltips
      ipElement.title = "Click to copy full IP address";

      // Add click to copy functionality
      const copyHandler = async () => {
        try {
          await navigator.clipboard.writeText(fullIP);
          const originalText = ipElement.innerText;
          ipElement.innerText = "Copied!";
          setTimeout(() => {
            ipElement.innerText = originalText;
          }, 1000);
        } catch (err) {
          console.error("Failed to copy:", err);
        }
      };
      ipElement.removeEventListener("click", copyHandler);
      ipElement.addEventListener("click", copyHandler);
    } catch (error) {
      document.getElementById("ip-address").innerText = "Failed to fetch";
      document.getElementById("warp-status").innerText = "unknown";
    }
  }

  function getRiskColorAndLevel(difficulty) {
    if (!difficulty || difficulty === "N/A") {
      return {
        color: "#e63946",
        level: "Unknown",
        ipQuality: "Unknown",
        percentage: 0,
      };
    }

    // Clean the difficulty hex string and get its length
    const cleanDifficulty = difficulty.replace("0x", "").replace(/^0+/, "");
    const hexLength = cleanDifficulty.length;
    const numericDifficulty = parseInt(difficulty.replace("0x", ""), 16);

    // Define risk levels based on hex length
    // Shorter hex = higher numeric value = lower difficulty = higher risk
    if (hexLength <= 2) {  // 0x00 to 0xFF
      return {
        color: "#e63946", // Red
        level: "Critical",
        ipQuality: "Very High Risk",
        percentage: 100,
      };
    } else if (hexLength <= 3) {  // 0x100 to 0xFFF
      return {
        color: "#FAB12F", // Orange
        level: "Hard",
        ipQuality: "High Risk",
        percentage: 75,
      };
    } else if (hexLength <= 4) {  // 0x1000 to 0xFFFF
      return {
        color: "#859F3D", // Light Green
        level: "Medium",
        ipQuality: "Medium Risk",
        percentage: 50,
      };
    } else if (hexLength <= 5) {  // 0x10000 to 0xFFFFF
      return {
        color: "#2a9d8f", // Teal
        level: "Easy",
        ipQuality: "Low Risk",
        percentage: 25,
      };
    } else {  // 0x100000 and above
      return {
        color: "#4CAF50", // Green
        level: "Very Easy",
        ipQuality: "Minimal Risk",
        percentage: 0,
      };
    }
  }

  function updateProgressBars(difficulty) {
    const powBar = document.getElementById("pow-bar");
    const powLevel = document.getElementById("pow-level");
    const ipQualityBar = document.getElementById("ip-quality-bar");
    const ipQuality = document.getElementById("ip-quality");
    const difficultyElement = document.getElementById("difficulty");
    const ipAddressElement = document.getElementById("ip-address");

    const {
      color,
      level,
      ipQuality: quality,
      percentage,
    } = getRiskColorAndLevel(difficulty);

    // Create gradient based on risk level
    const gradient = `linear-gradient(90deg,
            ${color} ${percentage}%,
            rgba(255, 255, 255, 0.1) ${percentage}%
        )`;

    // Update progress bars with title for explanation
    setProgressBar(
      powBar,
      powLevel,
      percentage,
      level,
      gradient,
      "PoW Difficulty: Required computational work before sending messages. Lower (green) means faster responses.",
    );
    setProgressBar(
      ipQualityBar,
      ipQuality,
      percentage,
      quality,
      gradient,
      "IP Quality: Indicates the risk associated with your IP as assessed by ChatGPT. Lower(green) is better.",
    );

    // Sync colors with text
    difficultyElement.style.color = color;
    ipAddressElement.style.color = color;
    ipQuality.style.color = color;
    powLevel.style.color = color;

    // Update the icon color
    const gradientStops = collapsedIndicator.querySelector("#gradient");
    if (gradientStops) {
      gradientStops.innerHTML = `
                <stop offset="0%" style="stop-color:${color};stop-opacity:1" />
                <stop offset="100%" style="stop-color:${color};stop-opacity:0.8" />
            `;
    }
  }

  function setProgressBar(bar, label, percentage, text, gradient, title) {
    bar.style.width = "100%";
    bar.style.background = gradient;
    bar.title = title; // Set title for explanation
    label.innerText = text;
    label.style.color = "var(--success-color, #10a37f)";
  }

  async function fetchChatGPTStatus() {
    try {
      // Create an AbortController for timeout
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 3000);

      const response = await fetch(
        "https://status.openai.com/api/v2/status.json",
        {
          signal: controller.signal,
        },
      );
      clearTimeout(timeoutId);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();
      const status = data.status;

      const statusDescription = document.getElementById("status-description");
      const statusMonitorItem = statusDescription.closest(".monitor-item");
      statusMonitorItem.style.display = "block";

      if (status) {
        const indicator = status.indicator.toLowerCase();
        const description = status.description || "All Systems Operational";

        const indicatorColors = {
          none: "#4CAF50",
          minor: "#FAB12F",
          major: "#FFA500",
          critical: "#e63946",
        };

        statusDescription.style.color = indicatorColors[indicator] || "#aaa";
        statusDescription.textContent = description;
      }
    } catch (error) {
      console.error("Error fetching ChatGPT status:", error);
      const statusDescription = document.getElementById("status-description");
      const statusMonitorItem = statusDescription.closest(".monitor-item");
      statusMonitorItem.style.display = "none";
    }
  }

  // Update user type display with colors
  function updateUserType(type) {
    const userTypeElement = document.getElementById("user-type");
    if (type === "plus" || type === "chatgpt-paid" || type.includes("paid")) {
      userTypeElement.textContent = "Paid";
      userTypeElement.style.color = "var(--success-color, #10a37f)";
    } else {
      userTypeElement.textContent = "Free";
      userTypeElement.style.color = "var(--success-color, #10a37f)";
    }
  }

  // Modify the fetch interceptor to update user type
  const originalFetch = window.fetch;
  window.fetch = async function (resource, options) {
    const response = await originalFetch(resource, options);

    if (
      (resource.includes("/backend-api/sentinel/chat-requirements") ||
        resource.includes("backend-anon/sentinel/chat-requirements")) &&
      options.method === "POST"
    ) {
      try {
        const clonedResponse = response.clone();
        const data = await clonedResponse.json();
        const difficulty = data.proofofwork?.difficulty || "N/A";
        const userType = data.persona || "free";

        document.getElementById("difficulty").innerText = difficulty;
        updateUserType(userType);
        updateProgressBars(difficulty);
      } catch (error) {
        console.error("Error processing response:", error);
      }
    }
    return response;
  };

  function updateTheme() {
    const isDark =
      document.documentElement.classList.contains("dark") ||
      localStorage.getItem("theme") === "dark";
    displayBox.style.backgroundColor = isDark
      ? "rgba(0, 0, 0, 0.8)"
      : "rgba(255, 255, 255, 0.9)";
    displayBox.style.color = isDark ? "#fff" : "#000";

    const labels = displayBox.querySelectorAll(".label");
    labels.forEach((label) => {
      label.style.color = isDark ? "#aaa" : "#666";
    });
  }

  // Initialize
  fetchIPInfo();
  fetchChatGPTStatus();
  updateTheme();

  const statusCheckInterval = 60 * 60 * 1000;
  let statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);

  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible") {
      clearInterval(statusCheckTimer);
      fetchChatGPTStatus();
      statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);
    }
  });

  const observer = new MutationObserver(updateTheme);
  observer.observe(document.documentElement, {
    attributes: true,
    attributeFilter: ["class"],
  });

  document.querySelectorAll("[title]").forEach((element) => {
    element.addEventListener("mouseenter", () => {
      element.style.transitionDelay = "0s"; // Remove delay
    });
  });
})();