ChatGPT 服務降級監控

監控 ChatGPT 服務狀態、IP 質量和 PoW 難度

// ==UserScript==
// @name         ChatGPT Degraded
// @name:zh-CN   ChatGPT 服务降级监控
// @name:zh-TW   ChatGPT 服務降級監控
// @namespace    https://github.com/lroolle/chatgpt-degraded
// @version      0.2.7
// @description  Monitor ChatGPT service level, IP quality and PoW difficulty
// @description:zh-CN  监控 ChatGPT 服务状态、IP 质量和 PoW 难度
// @description:zh-TW  監控 ChatGPT 服務狀態、IP 質量和 PoW 難度
// @author       lroolle
// @license      AGPL-3.0
// @match        *://chat.openai.com/*
// @match        *://chatgpt.com/*
// @connect      status.openai.com
// @connect      scamalytics.com
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-start
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojMmE5ZDhmO3N0b3Atb3BhY2l0eToxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3R5bGU9InN0b3AtY29sb3I6IzJhOWQ4ZjtzdG9wLW9wYWNpdHk6MC44Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Zz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjI4IiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KPCEtLU91dGVyIGNpcmNsZSBtb2RpZmllZCB0byBsb29rIGxpa2UgIkMiLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEyNSA1NSIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjIwIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjQiIGZpbGw9IiNmZmYiLz4KICA8L2c+Cjwvc3ZnPg==
// @homepageURL  https://github.com/lroolle/chatgpt-degraded
// @supportURL   https://github.com/lroolle/chatgpt-degraded/issues
// ==/UserScript==

(function () {
  "use strict";

  let displayBox, collapsedIndicator;

  const i18n = {
    'en': {
      service: 'Service',
      ip: 'IP',
      pow: 'PoW',
      status: 'Status',
      unknown: 'Unknown',
      copyHistory: 'Click to copy history',
      historyCopied: 'History copied!',
      copyFailed: 'Copy failed',
      riskLevels: {
        veryEasy: 'Very Easy',
        easy: 'Easy',
        medium: 'Medium',
        hard: 'Hard',
        critical: 'Critical'
      },
      tooltips: {
        powDifficulty: 'PoW Difficulty: Lower (green) means faster responses.',
        ipHistory: 'IP History (recent 10):',
        warpPlus: 'Protected by Cloudflare WARP+',
        warp: 'Protected by Cloudflare WARP',
        clickToCopy: 'Click to copy full history'
      }
    },
    'zh-CN': {
      service: '服务',
      ip: 'IP',
      pow: '算力',
      status: '状态',
      unknown: '未知',
      copyHistory: '点击复制历史',
      historyCopied: '已复制历史!',
      copyFailed: '复制失败',
      riskLevels: {
        veryEasy: '非常容易',
        easy: '容易',
        medium: '中等',
        hard: '困难',
        critical: '严重'
      },
      tooltips: {
        powDifficulty: 'PoW 难度:越低(绿色)响应越快',
        ipHistory: 'IP 历史(最近10条):',
        warpPlus: '已启用 Cloudflare WARP+',
        warp: '已启用 Cloudflare WARP',
        clickToCopy: '点击复制完整历史'
      }
    },
    'zh-TW': {
      service: '服務',
      ip: 'IP',
      pow: '算力',
      status: '狀態',
      unknown: '未知',
      copyHistory: '點擊複製歷史',
      historyCopied: '已複製歷史!',
      copyFailed: '複製失敗',
      riskLevels: {
        veryEasy: '非常容易',
        easy: '容易',
        medium: '中等',
        hard: '困難',
        critical: '嚴重'
      },
      tooltips: {
        powDifficulty: 'PoW 難度:越低(綠色)回應越快',
        ipHistory: 'IP 歷史(最近10筆):',
        warpPlus: '已啟用 Cloudflare WARP+',
        warp: '已啟用 Cloudflare WARP',
        clickToCopy: '點擊複製完整歷史'
      }
    }
  };

  // Get user language
  const userLang = (navigator.language || 'en').toLowerCase();
  const lang = i18n[userLang] ? userLang : 
               userLang.startsWith('zh-tw') ? 'zh-TW' :
               userLang.startsWith('zh') ? 'zh-CN' : 'en';
  const t = key => {
    const keys = key.split('.');
    return keys.reduce((obj, k) => obj?.[k], i18n[lang]) || i18n.en[keys[keys.length-1]];
  };

  function updateUserType(type) {
    const userTypeElement = document.getElementById("user-type");
    if (!userTypeElement) return;
    const isPaid =
      type &&
      (type === "plus" ||
        type === "chatgpt-paid" ||
        type.includes("paid") ||
        type.includes("premium") ||
        type.includes("pro"));
    userTypeElement.textContent = isPaid ? "Paid" : "Free";
    userTypeElement.dataset.tooltip = `ChatGPT Account Type: ${isPaid ? "Paid" : "Free"}`;
    userTypeElement.style.color = isPaid
      ? "var(--success-color, #10a37f)"
      : "var(--text-primary, #374151)";
  }

  function getRiskColorAndLevel(difficulty) {
    if (!difficulty || difficulty === "N/A") {
      return { color: "#e63946", level: "Unknown", percentage: 0 };
    }
    const cleanDifficulty = difficulty.replace(/^0x/, "").replace(/^0+/, "");
    const hexLength = cleanDifficulty.length;
    if (hexLength <= 2) {
      return { color: "#e63946", level: "Critical", percentage: 100 };
    } else if (hexLength <= 3) {
      return { color: "#FAB12F", level: "Hard", percentage: 75 };
    } else if (hexLength <= 4) {
      return { color: "#859F3D", level: "Medium", percentage: 50 };
    } else if (hexLength <= 5) {
      return { color: "#2a9d8f", level: "Easy", percentage: 25 };
    } else {
      return { color: "#4CAF50", level: "Very Easy", percentage: 0 };
    }
  }

  function setProgressBar(bar, label, percentage, text, gradient, title) {
    bar.style.width = "100%";
    bar.style.background = gradient;
    bar.dataset.tooltip = title;
    label.innerText = text;
  }

  function updateProgressBars(difficulty) {
    const powBar = document.getElementById("pow-bar");
    const powLevel = document.getElementById("pow-level");
    const difficultyElement = document.getElementById("difficulty");
    if (!powBar || !powLevel || !difficultyElement) return;
    const { color, level, percentage } = getRiskColorAndLevel(difficulty);
    const gradient = `linear-gradient(90deg, ${color} ${percentage}%, rgba(255, 255, 255, 0.1) ${percentage}%)`;
    setProgressBar(
      powBar,
      powLevel,
      percentage,
      level,
      gradient,
      "PoW Difficulty: Lower (green) means faster responses.",
    );
    difficultyElement.style.color = color;
    powLevel.style.color = color;

    // Update icon animation based on difficulty
    if (collapsedIndicator) {
      const outerRingAnim = collapsedIndicator.querySelector("#outer-ring-anim");
      const middleRingAnim = collapsedIndicator.querySelector("#middle-ring-anim");
      const centerDotAnim = collapsedIndicator.querySelector("#center-dot-anim");
      const gradientStops = collapsedIndicator.querySelector("#gradient");

      // Adjust animation speed based on difficulty level
      const animationSpeed = percentage < 25 ? 0.5 : percentage / 25; // Make it more still when easy
      if (outerRingAnim) outerRingAnim.setAttribute("dur", `${8/animationSpeed}s`);
      if (middleRingAnim) middleRingAnim.setAttribute("dur", `${4/animationSpeed}s`);
      if (centerDotAnim) {
        centerDotAnim.setAttribute("dur", `${2/animationSpeed}s`);
        // Smaller pulse for easy difficulty
        centerDotAnim.setAttribute("values", percentage < 25 ? "4;4.5;4" : "4;5;4");
      }

      // Update color
      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" />
        `;
      }
    }
  }

  const originalFetch = unsafeWindow.fetch;
  unsafeWindow.fetch = async function (resource, options) {
    const response = await originalFetch(resource, options);
    const url = typeof resource === "string" ? resource : resource?.url;
    const isChatRequirements =
      url &&
      (url.includes("/backend-api/sentinel/chat-requirements") ||
        url.includes("/backend-anon/sentinel/chat-requirements") ||
        url.includes("/api/sentinel/chat-requirements")) &&
      options?.method === "POST";
    if (isChatRequirements) {
      try {
        const clonedResponse = response.clone();
        const data = await clonedResponse.json();
        const difficulty = data?.proofofwork?.difficulty;
        const userType = data?.persona || data?.user_type || data?.account_type;
        const difficultyElement = document.getElementById("difficulty");
        if (difficultyElement) {
          if (difficulty) {
            difficultyElement.innerText = difficulty;
            difficultyElement.dataset.tooltip = `Raw Difficulty Value: ${difficulty}`;
            // Update IP log with new PoW difficulty
            const ipElement = document.getElementById("ip-address");
            if (ipElement) {
              const fullIP = ipElement.dataset.fullIp;
              const ipQualityElement = document.getElementById("ip-quality");
              const score = ipQualityElement ? parseInt(ipQualityElement.dataset.score) : null;
              if (fullIP) {
                const logs = addIPLog(fullIP, score, difficulty);
                const formattedLogs = formatIPLogs(logs);
                const ipContainerTooltip = [
                  "IP History (recent 10):",
                  formattedLogs,
                  "\n---",
                  "Click to copy history"
                ].join('\n');
                ipElement.dataset.tooltip = ipContainerTooltip;
              }
            }
          } else {
            difficultyElement.innerText = "N/A";
            difficultyElement.dataset.tooltip = "No difficulty value found";
          }
        }
        updateUserType(userType || "free");
        updateProgressBars(difficulty || "N/A");
      } catch (error) {
        const difficultyElement = document.getElementById("difficulty");
        if (difficultyElement) {
          difficultyElement.innerText = "N/A";
          difficultyElement.dataset.tooltip = `Error: ${error.message}`;
        }
        updateUserType("free");
        updateProgressBars("N/A");
      }
    }
    return response;
  };

  function initUI() {
    displayBox = document.createElement("div");
    displayBox.style.position = "fixed";
    displayBox.style.bottom = "10px";
    displayBox.style.right = "80px";
    displayBox.style.width = "360px";
    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">${t('service')}</span>
            <span id="user-type" class="value" data-tooltip="ChatGPT Account Type"></span>
          </div>
        </div>

        <!-- Proof of Work Difficulty -->
        <div class="monitor-item">
          <div class="monitor-row">
            <span class="label">${t('pow')}</span>
            <div class="pow-container">
              <span id="difficulty" class="value monospace" data-tooltip="PoW Difficulty Value"></span>
              <span id="pow-level" class="value-tag" data-tooltip="Difficulty Level"></span>
            </div>
          </div>
          <div class="progress-wrapper" data-tooltip="${t('tooltips.powDifficulty')}">
            <div class="progress-container">
              <div id="pow-bar" class="progress-bar"></div>
            </div>
            <div class="progress-background"></div>
          </div>
        </div>

        <!-- IP + IP Quality -->
        <div class="monitor-item">
          <div class="monitor-row">
            <span class="label">${t('ip')}</span>
            <div class="ip-container">
              <span id="ip-address" class="value monospace" data-tooltip="Click to copy IP address"></span>
              <span id="warp-badge" class="warp-badge"></span>
              <span id="ip-quality" class="value-tag" data-tooltip="IP Risk Info (Scamlytics)"></span>
            </div>
          </div>
        </div>

        <!-- OpenAI System Status -->
        <div class="monitor-item">
          <div class="monitor-row">
            <span class="label">${t('status')}</span>
            <a id="status-description"
               href="https://status.openai.com"
               target="_blank"
               class="value"
               data-tooltip="Click to open status.openai.com">
               ${t('unknown')}
            </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;
        }
        .monitor-row:last-child {
          margin-bottom: 4px;
        }
        .label {
          font-size: 14px;
          color: var(--text-secondary, #6B7280);
          flex-shrink: 0;
          min-width: 40px;
        }
        .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;
          transition: opacity 0.15s ease;
          cursor: pointer;
          display: inline-block;
        }
        .value-tag:hover {
          opacity: 0.8;
        }
        .progress-wrapper {
          position: relative;
          margin-left: 40px;
          margin-top: 4px;
        }
        .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;
        }
        /* Ensure IP risk level (ip-quality) is right-aligned, just like pow-level */
        #ip-quality {
          margin-left: auto;
        }
        .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;
        }
        .ip-container .value-tag {
          padding-right: 0;
          position: relative;
        }
        /* Special handling for IP Risk tooltip */
        .ip-container .value-tag[data-tooltip]::after {
          left: auto;
          right: 0;
          transform: translateY(4px);
        }
        .ip-container .value-tag[data-tooltip]:hover::after {
          transform: translateY(0);
          left: auto;
          right: 0;
        }
        /* General tooltip styles */
        [data-tooltip] {
          position: relative;
          cursor: help;
        }
        [data-tooltip]::after {
          content: attr(data-tooltip);
          position: absolute;
          bottom: 100%;
          left: 50%;
          transform: translateX(-50%) translateY(4px);
          background: var(--surface-primary, rgba(0, 0, 0, 0.8));
          color: #fff;
          padding: 12px 16px;
          border-radius: 6px;
          font-size: 12px;
          white-space: pre-line;
          width: max-content;
          max-width: 600px;
          min-width: 450px;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
          z-index: 1000;
          pointer-events: none;
          margin-bottom: 8px;
          opacity: 0;
          transition: opacity 0.15s ease, transform 0.15s ease;
          font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
        }
        [data-tooltip]:hover::after {
          opacity: 1;
          transform: translateX(-50%) translateY(0);
        }
        /* Arrow styles */
        [data-tooltip]::before {
          content: '';
          position: absolute;
          bottom: 100%;
          left: 50%;
          transform: translateX(-50%) translateY(4px);
          border: 6px solid transparent;
          border-top-color: var(--surface-primary, rgba(0, 0, 0, 0.8));
          margin-bottom: -4px;
          pointer-events: none;
          opacity: 0;
          transition: opacity 0.15s ease, transform 0.15s ease;
        }
        [data-tooltip]:hover::before {
          opacity: 1;
          transform: translateY(0);
        }
        /* Special handling for IP Risk tooltip arrow */
        .ip-container .value-tag[data-tooltip]::before {
          left: auto;
          right: 12px;
          transform: translateY(4px);
        }
        .ip-container .value-tag[data-tooltip]:hover::before {
          transform: translateY(0);
          left: auto;
          right: 12px;
        }
        /* Ensure tooltips don't get cut off at viewport edges */
        @media screen and (max-width: 768px) {
          [data-tooltip]::after {
            min-width: 300px;
            max-width: calc(100vw - 48px);
          }
        }
      </style>
    `;
    document.body.appendChild(displayBox);

    collapsedIndicator = document.createElement("div");
    collapsedIndicator.style.position = "fixed";
    collapsedIndicator.style.bottom = "10px";
    collapsedIndicator.style.right = "40px";
    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:#666;stop-opacity:1" />
            <stop offset="100%" style="stop-color:#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="stroke-dashoffset"
                     dur="4s"
                     values="0;120"
                     repeatCount="indefinite"
                     id="outer-ring-anim"/>
          </circle>
          <circle cx="32" cy="32" r="12" fill="none" stroke="#fff" stroke-width="1">
            <animate attributeName="r"
                     dur="2s"
                     values="12;14;12"
                     repeatCount="indefinite"
                     id="middle-ring-anim"/>
          </circle>
          <circle id="center-dot" cx="32" cy="32" r="4" fill="#fff">
            <animate attributeName="r"
                     dur="1s"
                     values="4;5;4"
                     repeatCount="indefinite"
                     id="center-dot-anim"/>
          </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);
    });

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

    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);
      }
    });
  }

  if (document.readyState !== "loading") {
    initUI();
  } else {
    document.addEventListener("DOMContentLoaded", initUI);
  }

  function maskIP(ip) {
    if (!ip || ip === "Unknown") return ip;
    if (ip.includes(".")) {
      const parts = ip.split(".");
      if (parts.length === 4) {
        return `${parts[0]}.*.*.${parts[3]}`;
      }
    }
    if (ip.includes(":")) {
      const parts = ip.split(":");
      // Shorten IPv6 to just show first and last part
      if (parts.length > 2) {
        return `${parts[0]}:*:${parts[parts.length - 1]}`;
      }
    }
    return ip;
  }

  async function fetchIPQuality(ip) {
    try {
      const response = await new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: `https://scamalytics.com/ip/${ip}`,
          timeout: 3000,
          onload: (r) =>
            r.status === 200
              ? resolve(r.responseText)
              : reject(new Error(`HTTP ${r.status}`)),
          onerror: reject,
          ontimeout: () => reject(new Error("Request timed out")),
        });
      });
      const parser = new DOMParser();
      const doc = parser.parseFromString(response, "text/html");
      const scoreElement = doc.querySelector(".score_bar .score");
      const scoreMatch =
        scoreElement?.textContent.match(/Fraud Score:\s*(\d+)/i);
      if (!scoreMatch) {
        return {
          label: "Unknown",
          color: "#aaa",
          tooltip: "Could not determine IP quality",
          score: null
        };
      }
      const score = parseInt(scoreMatch[1], 10);
      const riskElement = doc.querySelector(".panel_title");
      const riskText = riskElement?.textContent.trim() || "Unknown Risk";
      const panelColor = riskElement?.style.backgroundColor || "#aaa";
      const descriptionElement = doc.querySelector(".panel_body");
      const description = descriptionElement?.textContent.trim() || "";
      const trimmedDescription =
        description.length > 150
          ? `${description.substring(0, 147)}...`
          : description;

      function extractTableValue(header) {
        const row = Array.from(doc.querySelectorAll("th")).find(
          (th) => th.textContent.trim() === header,
        )?.parentElement;
        return row?.querySelector("td")?.textContent.trim() || null;
      }
      function isRiskYes(header) {
        const row = Array.from(doc.querySelectorAll("th")).find(
          (th) => th.textContent.trim() === header,
        )?.parentElement;
        return row?.querySelector(".risk.yes") !== null;
      }
      const details = {
        location: extractTableValue("City") || "Unknown",
        state: extractTableValue("State / Province"),
        country: extractTableValue("Country Name"),
        isp: extractTableValue("ISP Name") || "Unknown",
        organization: extractTableValue("Organization Name"),
        isVPN: isRiskYes("Anonymizing VPN"),
        isTor: isRiskYes("Tor Exit Node"),
        isServer: isRiskYes("Server"),
        isProxy:
          isRiskYes("Public Proxy") ||
          isRiskYes("Web Proxy") ||
          isRiskYes("Proxy"),
      };
      let label, color;
      if (riskText && riskText !== "Unknown Risk") {
        label = riskText;
        color = panelColor !== "#aaa" ? panelColor : getColorForScore(score);
      } else {
        ({ label, color } = getLabelAndColorForScore(score));
      }
      const warnings = [];
      if (details.isVPN) warnings.push("VPN");
      if (details.isTor) warnings.push("Tor");
      if (details.isServer) warnings.push("Server");
      if (details.isProxy) warnings.push("Proxy");
      const location = [details.location, details.state, details.country]
        .filter(Boolean)
        .join(", ");
      const tooltip = [
        "IP Risk Info (Scamlytics):",
        label !== "Unknown" ? `Risk: ${label} (${score}/100)` : "",
        `Location: ${location}`,
        `ISP: ${details.isp}${details.organization ? ` (${details.organization})` : ""}`,
        warnings.length ? `Warnings: ${warnings.join(", ")}` : "",
        trimmedDescription ? `\n${trimmedDescription}` : "",
        "\nClick to view full analysis",
      ]
        .filter(Boolean)
        .join("\n");
      return { label, color, tooltip, score };
    } catch (error) {
      return {
        label: "Unknown",
        color: "#aaa",
        tooltip: "Could not check IP quality",
        score: null
      };
    }
  }

  function getColorForScore(score) {
    if (score < 25) return "#4CAF50";
    if (score < 50) return "#859F3D";
    if (score < 75) return "#FAB12F";
    return "#e63946";
  }

  function getLabelAndColorForScore(score) {
    if (score < 25) return { label: t('riskLevels.veryEasy'), color: "#4CAF50" };
    if (score < 50) return { label: t('riskLevels.easy'), color: "#859F3D" };
    if (score < 75) return { label: t('riskLevels.medium'), color: "#FAB12F" };
    return { label: t('riskLevels.critical'), color: "#e63946" };
  }

  function getIPLogs() {
    try {
      const logs = localStorage.getItem('chatgpt_ip_logs');
      return logs ? JSON.parse(logs) : [];
    } catch (error) {
      console.error('Error reading IP logs:', error);
      return [];
    }
  }

  function addIPLog(ip, score, difficulty) {
    try {
      const logs = getIPLogs();
      const timestamp = new Date().toISOString();
      const newLog = { timestamp, ip, score, difficulty };
      if (logs.length > 0 && logs[0].ip === ip) {
        logs[0] = newLog;
      } else {
        logs.unshift(newLog);
      }
      const trimmedLogs = logs.slice(0, 10);
      localStorage.setItem('chatgpt_ip_logs', JSON.stringify(trimmedLogs));
      return trimmedLogs;
    } catch (error) {
      console.error('Error adding IP log:', error);
      return [];
    }
  }

  function formatIPLogs(logs) {
    return logs.map(log => {
      const date = new Date(log.timestamp);
      const formattedDate = date.toLocaleString('en-US', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      }).replace(/(\d+)\/(\d+)\/(\d+),\s(\d+):(\d+)/, '[$3-$1-$2 $4:$5]');
      const { color: powColor, level: powLevel } = getRiskColorAndLevel(log.difficulty);
      const scoreDisplay = log.score !== null && log.score !== undefined ? log.score : 'N/A';
      return `${formattedDate} ${log.ip}(${scoreDisplay}), ${log.difficulty || 'N/A'}(${powLevel})`;
    }).join('\n');
  }

  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 ipQualityElement = document.getElementById("ip-quality");
      if (!ipElement || !warpBadge || !ipQualityElement) return;

      const maskedIP = maskIP(data.ip);
      const fullIP = data.ip || "Unknown";
      const warpStatus = data.warp || "off";
      ipElement.innerText = maskedIP;
      ipElement.dataset.fullIp = fullIP;

      if (warpStatus === "on" || warpStatus === "plus") {
        warpBadge.style.display = "inline-flex";
        warpBadge.innerText = warpStatus === "plus" ? "warp+" : "warp";
        warpBadge.dataset.tooltip = `Protected by Cloudflare WARP${warpStatus === "plus" ? "+" : ""}`;
      } else {
        warpBadge.style.display = "none";
      }

      const { label, color, tooltip, score } = await fetchIPQuality(fullIP);
      ipElement.style.color = color;
      ipQualityElement.innerText = score !== null ? `${label} (${score})` : label;
      ipQualityElement.style.color = color;
      ipQualityElement.dataset.score = score;

      const difficultyElement = document.getElementById("difficulty");
      const currentDifficulty = difficultyElement?.innerText || "N/A";
      const logs = addIPLog(fullIP, score, currentDifficulty);
      const formattedLogs = formatIPLogs(logs);
      const ipContainerTooltip = [
        "IP History (recent 10):",
        formattedLogs,
        "\n---",
        "Click to copy full history"
      ].join('\n');
      ipElement.dataset.tooltip = ipContainerTooltip;
      ipQualityElement.dataset.tooltip = tooltip;

      ipQualityElement.onclick = () =>
        window.open(`https://scamalytics.com/ip/${fullIP}`, "_blank");

      const copyHandler = async () => {
        try {
          const logs = getIPLogs();
          const formattedHistory = formatIPLogs(logs);
          await navigator.clipboard.writeText(formattedHistory);
          const originalText = ipElement.innerText;
          ipElement.innerText = "History copied!";
          setTimeout(() => {
            ipElement.innerText = originalText;
          }, 1000);
        } catch (err) {
          ipElement.innerText = "Copy failed";
          setTimeout(() => {
            ipElement.innerText = maskedIP;
          }, 1000);
        }
      };
      ipElement.removeEventListener("click", copyHandler);
      ipElement.addEventListener("click", copyHandler);
    } catch (error) {
      const ipElement = document.getElementById("ip-address");
      const warpBadge = document.getElementById("warp-badge");
      const ipQualityElement = document.getElementById("ip-quality");
      if (ipElement) ipElement.innerText = "Failed to fetch";
      if (warpBadge) warpBadge.style.display = "none";
      if (ipQualityElement) {
        ipQualityElement.innerText = "Unknown";
        ipQualityElement.style.color = "#aaa";
        ipQualityElement.dataset.tooltip = "Could not check IP quality";
      }
    }
  }

  async function fetchChatGPTStatus() {
    try {
      if (typeof GM_xmlhttpRequest === "undefined") {
        throw new Error("GM_xmlhttpRequest not supported");
      }
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: "https://status.openai.com/api/v2/status.json",
          timeout: 3000,
          ontimeout: () => reject(new Error("Status check timed out")),
          onload: (response) => {
            if (response.status === 200) {
              try {
                const data = JSON.parse(response.responseText);
                const status = data.status;
                const statusDescription =
                  document.getElementById("status-description");
                const statusMonitorItem =
                  statusDescription?.closest(".monitor-item");
                if (!statusDescription || !statusMonitorItem) {
                  reject(new Error("Status UI elements not found"));
                  return;
                }
                statusMonitorItem.style.display = "block";
                if (status) {
                  const indicator = (status.indicator || "").toLowerCase();
                  const description =
                    status.description || "All Systems Operational";
                  const indicatorColors = {
                    none: "var(--success-color, #10a37f)",
                    minor: "#FAB12F",
                    major: "#FFA500",
                    critical: "#e63946",
                  };
                  if (description === "All Systems Operational") {
                    statusDescription.style.color =
                      "var(--success-color, #10a37f)";
                  } else {
                    statusDescription.style.color =
                      indicatorColors[indicator] || "#aaa";
                  }
                  statusDescription.textContent = description;
                }
                resolve();
              } catch (err) {
                reject(err);
              }
            } else {
              reject(new Error(`HTTP error: ${response.status}`));
            }
          },
          onerror: (err) => reject(err),
        });
      });
    } catch (error) {
      const statusDescription = document.getElementById("status-description");
      const statusMonitorItem = statusDescription?.closest(".monitor-item");
      if (statusMonitorItem) statusMonitorItem.style.display = "none";
    }
  }

  function updateTheme() {
    const isDark =
      document.documentElement.classList.contains("dark") ||
      localStorage.getItem("theme") === "dark" ||
      document.documentElement.dataset.theme === "dark";
    displayBox.style.backgroundColor = isDark
      ? "var(--surface-primary, rgba(0, 0, 0, 0.8))"
      : "var(--surface-primary, rgba(255, 255, 255, 0.9))";
    displayBox.style.color = isDark
      ? "var(--text-primary, #fff)"
      : "var(--text-primary, #000)";
    displayBox.querySelectorAll(".label").forEach((label) => {
      label.style.color = isDark
        ? "var(--text-secondary, #aaa)"
        : "var(--text-secondary, #666)";
    });
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址