ChatGPT 服务降级监控

监控 ChatGPT 服务状态、IP 质量和 PoW 难度

  1. // ==UserScript==
  2. // @name ChatGPT Degraded
  3. // @name:zh-CN ChatGPT 服务降级监控
  4. // @name:zh-TW ChatGPT 服務降級監控
  5. // @namespace https://github.com/lroolle/chatgpt-degraded
  6. // @version 0.2.7
  7. // @description Monitor ChatGPT service level, IP quality and PoW difficulty
  8. // @description:zh-CN 监控 ChatGPT 服务状态、IP 质量和 PoW 难度
  9. // @description:zh-TW 監控 ChatGPT 服務狀態、IP 質量和 PoW 難度
  10. // @author lroolle
  11. // @license AGPL-3.0
  12. // @match *://chat.openai.com/*
  13. // @match *://chatgpt.com/*
  14. // @connect status.openai.com
  15. // @connect scamalytics.com
  16. // @grant GM_xmlhttpRequest
  17. // @grant unsafeWindow
  18. // @run-at document-start
  19. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojMmE5ZDhmO3N0b3Atb3BhY2l0eToxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3R5bGU9InN0b3AtY29sb3I6IzJhOWQ4ZjtzdG9wLW9wYWNpdHk6MC44Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Zz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjI4IiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KPCEtLU91dGVyIGNpcmNsZSBtb2RpZmllZCB0byBsb29rIGxpa2UgIkMiLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEyNSA1NSIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjIwIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjQiIGZpbGw9IiNmZmYiLz4KICA8L2c+Cjwvc3ZnPg==
  20. // @homepageURL https://github.com/lroolle/chatgpt-degraded
  21. // @supportURL https://github.com/lroolle/chatgpt-degraded/issues
  22. // ==/UserScript==
  23.  
  24. (function () {
  25. "use strict";
  26.  
  27. let displayBox, collapsedIndicator;
  28.  
  29. const i18n = {
  30. 'en': {
  31. service: 'Service',
  32. ip: 'IP',
  33. pow: 'PoW',
  34. status: 'Status',
  35. unknown: 'Unknown',
  36. copyHistory: 'Click to copy history',
  37. historyCopied: 'History copied!',
  38. copyFailed: 'Copy failed',
  39. riskLevels: {
  40. veryEasy: 'Very Easy',
  41. easy: 'Easy',
  42. medium: 'Medium',
  43. hard: 'Hard',
  44. critical: 'Critical'
  45. },
  46. tooltips: {
  47. powDifficulty: 'PoW Difficulty: Lower (green) means faster responses.',
  48. ipHistory: 'IP History (recent 10):',
  49. warpPlus: 'Protected by Cloudflare WARP+',
  50. warp: 'Protected by Cloudflare WARP',
  51. clickToCopy: 'Click to copy full history'
  52. }
  53. },
  54. 'zh-CN': {
  55. service: '服务',
  56. ip: 'IP',
  57. pow: '算力',
  58. status: '状态',
  59. unknown: '未知',
  60. copyHistory: '点击复制历史',
  61. historyCopied: '已复制历史!',
  62. copyFailed: '复制失败',
  63. riskLevels: {
  64. veryEasy: '非常容易',
  65. easy: '容易',
  66. medium: '中等',
  67. hard: '困难',
  68. critical: '严重'
  69. },
  70. tooltips: {
  71. powDifficulty: 'PoW 难度:越低(绿色)响应越快',
  72. ipHistory: 'IP 历史(最近10条):',
  73. warpPlus: '已启用 Cloudflare WARP+',
  74. warp: '已启用 Cloudflare WARP',
  75. clickToCopy: '点击复制完整历史'
  76. }
  77. },
  78. 'zh-TW': {
  79. service: '服務',
  80. ip: 'IP',
  81. pow: '算力',
  82. status: '狀態',
  83. unknown: '未知',
  84. copyHistory: '點擊複製歷史',
  85. historyCopied: '已複製歷史!',
  86. copyFailed: '複製失敗',
  87. riskLevels: {
  88. veryEasy: '非常容易',
  89. easy: '容易',
  90. medium: '中等',
  91. hard: '困難',
  92. critical: '嚴重'
  93. },
  94. tooltips: {
  95. powDifficulty: 'PoW 難度:越低(綠色)回應越快',
  96. ipHistory: 'IP 歷史(最近10筆):',
  97. warpPlus: '已啟用 Cloudflare WARP+',
  98. warp: '已啟用 Cloudflare WARP',
  99. clickToCopy: '點擊複製完整歷史'
  100. }
  101. }
  102. };
  103.  
  104. // Get user language
  105. const userLang = (navigator.language || 'en').toLowerCase();
  106. const lang = i18n[userLang] ? userLang :
  107. userLang.startsWith('zh-tw') ? 'zh-TW' :
  108. userLang.startsWith('zh') ? 'zh-CN' : 'en';
  109. const t = key => {
  110. const keys = key.split('.');
  111. return keys.reduce((obj, k) => obj?.[k], i18n[lang]) || i18n.en[keys[keys.length-1]];
  112. };
  113.  
  114. function updateUserType(type) {
  115. const userTypeElement = document.getElementById("user-type");
  116. if (!userTypeElement) return;
  117. const isPaid =
  118. type &&
  119. (type === "plus" ||
  120. type === "chatgpt-paid" ||
  121. type.includes("paid") ||
  122. type.includes("premium") ||
  123. type.includes("pro"));
  124. userTypeElement.textContent = isPaid ? "Paid" : "Free";
  125. userTypeElement.dataset.tooltip = `ChatGPT Account Type: ${isPaid ? "Paid" : "Free"}`;
  126. userTypeElement.style.color = isPaid
  127. ? "var(--success-color, #10a37f)"
  128. : "var(--text-primary, #374151)";
  129. }
  130.  
  131. function getRiskColorAndLevel(difficulty) {
  132. if (!difficulty || difficulty === "N/A") {
  133. return { color: "#e63946", level: "Unknown", percentage: 0 };
  134. }
  135. const cleanDifficulty = difficulty.replace(/^0x/, "").replace(/^0+/, "");
  136. const hexLength = cleanDifficulty.length;
  137. if (hexLength <= 2) {
  138. return { color: "#e63946", level: "Critical", percentage: 100 };
  139. } else if (hexLength <= 3) {
  140. return { color: "#FAB12F", level: "Hard", percentage: 75 };
  141. } else if (hexLength <= 4) {
  142. return { color: "#859F3D", level: "Medium", percentage: 50 };
  143. } else if (hexLength <= 5) {
  144. return { color: "#2a9d8f", level: "Easy", percentage: 25 };
  145. } else {
  146. return { color: "#4CAF50", level: "Very Easy", percentage: 0 };
  147. }
  148. }
  149.  
  150. function setProgressBar(bar, label, percentage, text, gradient, title) {
  151. bar.style.width = "100%";
  152. bar.style.background = gradient;
  153. bar.dataset.tooltip = title;
  154. label.innerText = text;
  155. }
  156.  
  157. function updateProgressBars(difficulty) {
  158. const powBar = document.getElementById("pow-bar");
  159. const powLevel = document.getElementById("pow-level");
  160. const difficultyElement = document.getElementById("difficulty");
  161. if (!powBar || !powLevel || !difficultyElement) return;
  162. const { color, level, percentage } = getRiskColorAndLevel(difficulty);
  163. const gradient = `linear-gradient(90deg, ${color} ${percentage}%, rgba(255, 255, 255, 0.1) ${percentage}%)`;
  164. setProgressBar(
  165. powBar,
  166. powLevel,
  167. percentage,
  168. level,
  169. gradient,
  170. "PoW Difficulty: Lower (green) means faster responses.",
  171. );
  172. difficultyElement.style.color = color;
  173. powLevel.style.color = color;
  174.  
  175. // Update icon animation based on difficulty
  176. if (collapsedIndicator) {
  177. const outerRingAnim = collapsedIndicator.querySelector("#outer-ring-anim");
  178. const middleRingAnim = collapsedIndicator.querySelector("#middle-ring-anim");
  179. const centerDotAnim = collapsedIndicator.querySelector("#center-dot-anim");
  180. const gradientStops = collapsedIndicator.querySelector("#gradient");
  181.  
  182. // Adjust animation speed based on difficulty level
  183. const animationSpeed = percentage < 25 ? 0.5 : percentage / 25; // Make it more still when easy
  184. if (outerRingAnim) outerRingAnim.setAttribute("dur", `${8/animationSpeed}s`);
  185. if (middleRingAnim) middleRingAnim.setAttribute("dur", `${4/animationSpeed}s`);
  186. if (centerDotAnim) {
  187. centerDotAnim.setAttribute("dur", `${2/animationSpeed}s`);
  188. // Smaller pulse for easy difficulty
  189. centerDotAnim.setAttribute("values", percentage < 25 ? "4;4.5;4" : "4;5;4");
  190. }
  191.  
  192. // Update color
  193. if (gradientStops) {
  194. gradientStops.innerHTML = `
  195. <stop offset="0%" style="stop-color:${color};stop-opacity:1" />
  196. <stop offset="100%" style="stop-color:${color};stop-opacity:0.8" />
  197. `;
  198. }
  199. }
  200. }
  201.  
  202. const originalFetch = unsafeWindow.fetch;
  203. unsafeWindow.fetch = async function (resource, options) {
  204. const response = await originalFetch(resource, options);
  205. const url = typeof resource === "string" ? resource : resource?.url;
  206. const isChatRequirements =
  207. url &&
  208. (url.includes("/backend-api/sentinel/chat-requirements") ||
  209. url.includes("/backend-anon/sentinel/chat-requirements") ||
  210. url.includes("/api/sentinel/chat-requirements")) &&
  211. options?.method === "POST";
  212. if (isChatRequirements) {
  213. try {
  214. const clonedResponse = response.clone();
  215. const data = await clonedResponse.json();
  216. const difficulty = data?.proofofwork?.difficulty;
  217. const userType = data?.persona || data?.user_type || data?.account_type;
  218. const difficultyElement = document.getElementById("difficulty");
  219. if (difficultyElement) {
  220. if (difficulty) {
  221. difficultyElement.innerText = difficulty;
  222. difficultyElement.dataset.tooltip = `Raw Difficulty Value: ${difficulty}`;
  223. // Update IP log with new PoW difficulty
  224. const ipElement = document.getElementById("ip-address");
  225. if (ipElement) {
  226. const fullIP = ipElement.dataset.fullIp;
  227. const ipQualityElement = document.getElementById("ip-quality");
  228. const score = ipQualityElement ? parseInt(ipQualityElement.dataset.score) : null;
  229. if (fullIP) {
  230. const logs = addIPLog(fullIP, score, difficulty);
  231. const formattedLogs = formatIPLogs(logs);
  232. const ipContainerTooltip = [
  233. "IP History (recent 10):",
  234. formattedLogs,
  235. "\n---",
  236. "Click to copy history"
  237. ].join('\n');
  238. ipElement.dataset.tooltip = ipContainerTooltip;
  239. }
  240. }
  241. } else {
  242. difficultyElement.innerText = "N/A";
  243. difficultyElement.dataset.tooltip = "No difficulty value found";
  244. }
  245. }
  246. updateUserType(userType || "free");
  247. updateProgressBars(difficulty || "N/A");
  248. } catch (error) {
  249. const difficultyElement = document.getElementById("difficulty");
  250. if (difficultyElement) {
  251. difficultyElement.innerText = "N/A";
  252. difficultyElement.dataset.tooltip = `Error: ${error.message}`;
  253. }
  254. updateUserType("free");
  255. updateProgressBars("N/A");
  256. }
  257. }
  258. return response;
  259. };
  260.  
  261. function initUI() {
  262. displayBox = document.createElement("div");
  263. displayBox.style.position = "fixed";
  264. displayBox.style.bottom = "10px";
  265. displayBox.style.right = "80px";
  266. displayBox.style.width = "360px";
  267. displayBox.style.padding = "24px";
  268. displayBox.style.backgroundColor =
  269. "var(--surface-primary, rgb(255, 255, 255))";
  270. displayBox.style.color = "var(--text-primary, #374151)";
  271. displayBox.style.fontSize = "14px";
  272. displayBox.style.borderRadius = "16px";
  273. displayBox.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
  274. displayBox.style.zIndex = "10000";
  275. displayBox.style.transition = "opacity 0.15s ease, transform 0.15s ease";
  276. displayBox.style.display = "none";
  277. displayBox.style.opacity = "0";
  278. displayBox.style.transform = "translateX(10px)";
  279. displayBox.style.border =
  280. "1px solid var(--border-light, rgba(0, 0, 0, 0.05))";
  281.  
  282. displayBox.innerHTML = `
  283. <div id="content">
  284. <div class="monitor-item">
  285. <div class="monitor-row">
  286. <span class="label">${t('service')}</span>
  287. <span id="user-type" class="value" data-tooltip="ChatGPT Account Type"></span>
  288. </div>
  289. </div>
  290.  
  291. <!-- Proof of Work Difficulty -->
  292. <div class="monitor-item">
  293. <div class="monitor-row">
  294. <span class="label">${t('pow')}</span>
  295. <div class="pow-container">
  296. <span id="difficulty" class="value monospace" data-tooltip="PoW Difficulty Value"></span>
  297. <span id="pow-level" class="value-tag" data-tooltip="Difficulty Level"></span>
  298. </div>
  299. </div>
  300. <div class="progress-wrapper" data-tooltip="${t('tooltips.powDifficulty')}">
  301. <div class="progress-container">
  302. <div id="pow-bar" class="progress-bar"></div>
  303. </div>
  304. <div class="progress-background"></div>
  305. </div>
  306. </div>
  307.  
  308. <!-- IP + IP Quality -->
  309. <div class="monitor-item">
  310. <div class="monitor-row">
  311. <span class="label">${t('ip')}</span>
  312. <div class="ip-container">
  313. <span id="ip-address" class="value monospace" data-tooltip="Click to copy IP address"></span>
  314. <span id="warp-badge" class="warp-badge"></span>
  315. <span id="ip-quality" class="value-tag" data-tooltip="IP Risk Info (Scamlytics)"></span>
  316. </div>
  317. </div>
  318. </div>
  319.  
  320. <!-- OpenAI System Status -->
  321. <div class="monitor-item">
  322. <div class="monitor-row">
  323. <span class="label">${t('status')}</span>
  324. <a id="status-description"
  325. href="https://status.openai.com"
  326. target="_blank"
  327. class="value"
  328. data-tooltip="Click to open status.openai.com">
  329. ${t('unknown')}
  330. </a>
  331. </div>
  332. </div>
  333. </div>
  334.  
  335. <style>
  336. .monitor-item {
  337. margin-bottom: 16px;
  338. }
  339. .monitor-item:last-child {
  340. margin-bottom: 0;
  341. }
  342. .monitor-row {
  343. display: flex;
  344. align-items: center;
  345. gap: 6px;
  346. margin-bottom: 6px;
  347. }
  348. .monitor-row:last-child {
  349. margin-bottom: 4px;
  350. }
  351. .label {
  352. font-size: 14px;
  353. color: var(--text-secondary, #6B7280);
  354. flex-shrink: 0;
  355. min-width: 40px;
  356. }
  357. .value {
  358. font-size: 14px;
  359. color: var(--text-primary, #374151);
  360. flex: 1;
  361. }
  362. .monospace {
  363. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  364. font-size: 14px;
  365. }
  366. .value-tag {
  367. font-size: 14px;
  368. color: var(--success-color, #10a37f);
  369. white-space: nowrap;
  370. font-weight: 500;
  371. transition: opacity 0.15s ease;
  372. cursor: pointer;
  373. display: inline-block;
  374. }
  375. .value-tag:hover {
  376. opacity: 0.8;
  377. }
  378. .progress-wrapper {
  379. position: relative;
  380. margin-left: 40px;
  381. margin-top: 4px;
  382. }
  383. .progress-container {
  384. position: relative;
  385. height: 4px;
  386. background: transparent;
  387. border-radius: 2px;
  388. overflow: hidden;
  389. z-index: 1;
  390. }
  391. .progress-background {
  392. position: absolute;
  393. top: 0;
  394. left: 0;
  395. right: 0;
  396. height: 4px;
  397. background: var(--surface-secondary, rgba(0, 0, 0, 0.08));
  398. border-radius: 2px;
  399. }
  400. .progress-bar {
  401. height: 100%;
  402. width: 0%;
  403. transition: all 0.3s ease;
  404. background: var(--success-color, #10a37f);
  405. }
  406. #status-description {
  407. text-decoration: none;
  408. color: inherit;
  409. }
  410. #status-description:hover {
  411. text-decoration: underline;
  412. }
  413. #ip-address {
  414. cursor: pointer;
  415. }
  416. #ip-address:hover {
  417. opacity: 0.7;
  418. }
  419. #user-type {
  420. font-weight: 500;
  421. }
  422. .ip-container,
  423. .pow-container {
  424. display: flex;
  425. align-items: center;
  426. gap: 6px;
  427. flex: 1;
  428. }
  429. /* Ensure IP risk level (ip-quality) is right-aligned, just like pow-level */
  430. #ip-quality {
  431. margin-left: auto;
  432. }
  433. .warp-badge {
  434. font-size: 12px;
  435. color: var(--success-color, #10a37f);
  436. background-color: var(--surface-secondary, rgba(16, 163, 127, 0.1));
  437. padding: 2px 4px;
  438. border-radius: 4px;
  439. font-weight: 500;
  440. cursor: help;
  441. display: none;
  442. }
  443. .ip-container .value-tag {
  444. padding-right: 0;
  445. position: relative;
  446. }
  447. /* Special handling for IP Risk tooltip */
  448. .ip-container .value-tag[data-tooltip]::after {
  449. left: auto;
  450. right: 0;
  451. transform: translateY(4px);
  452. }
  453. .ip-container .value-tag[data-tooltip]:hover::after {
  454. transform: translateY(0);
  455. left: auto;
  456. right: 0;
  457. }
  458. /* General tooltip styles */
  459. [data-tooltip] {
  460. position: relative;
  461. cursor: help;
  462. }
  463. [data-tooltip]::after {
  464. content: attr(data-tooltip);
  465. position: absolute;
  466. bottom: 100%;
  467. left: 50%;
  468. transform: translateX(-50%) translateY(4px);
  469. background: var(--surface-primary, rgba(0, 0, 0, 0.8));
  470. color: #fff;
  471. padding: 12px 16px;
  472. border-radius: 6px;
  473. font-size: 12px;
  474. white-space: pre-line;
  475. width: max-content;
  476. max-width: 600px;
  477. min-width: 450px;
  478. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  479. z-index: 1000;
  480. pointer-events: none;
  481. margin-bottom: 8px;
  482. opacity: 0;
  483. transition: opacity 0.15s ease, transform 0.15s ease;
  484. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  485. }
  486. [data-tooltip]:hover::after {
  487. opacity: 1;
  488. transform: translateX(-50%) translateY(0);
  489. }
  490. /* Arrow styles */
  491. [data-tooltip]::before {
  492. content: '';
  493. position: absolute;
  494. bottom: 100%;
  495. left: 50%;
  496. transform: translateX(-50%) translateY(4px);
  497. border: 6px solid transparent;
  498. border-top-color: var(--surface-primary, rgba(0, 0, 0, 0.8));
  499. margin-bottom: -4px;
  500. pointer-events: none;
  501. opacity: 0;
  502. transition: opacity 0.15s ease, transform 0.15s ease;
  503. }
  504. [data-tooltip]:hover::before {
  505. opacity: 1;
  506. transform: translateY(0);
  507. }
  508. /* Special handling for IP Risk tooltip arrow */
  509. .ip-container .value-tag[data-tooltip]::before {
  510. left: auto;
  511. right: 12px;
  512. transform: translateY(4px);
  513. }
  514. .ip-container .value-tag[data-tooltip]:hover::before {
  515. transform: translateY(0);
  516. left: auto;
  517. right: 12px;
  518. }
  519. /* Ensure tooltips don't get cut off at viewport edges */
  520. @media screen and (max-width: 768px) {
  521. [data-tooltip]::after {
  522. min-width: 300px;
  523. max-width: calc(100vw - 48px);
  524. }
  525. }
  526. </style>
  527. `;
  528. document.body.appendChild(displayBox);
  529.  
  530. collapsedIndicator = document.createElement("div");
  531. collapsedIndicator.style.position = "fixed";
  532. collapsedIndicator.style.bottom = "10px";
  533. collapsedIndicator.style.right = "40px";
  534. collapsedIndicator.style.width = "24px";
  535. collapsedIndicator.style.height = "24px";
  536. collapsedIndicator.style.backgroundColor = "transparent";
  537. collapsedIndicator.style.border =
  538. "1px solid var(--token-border-light, rgba(0, 0, 0, 0.1))";
  539. collapsedIndicator.style.borderRadius = "50%";
  540. collapsedIndicator.style.cursor = "pointer";
  541. collapsedIndicator.style.zIndex = "10000";
  542. collapsedIndicator.style.display = "flex";
  543. collapsedIndicator.style.alignItems = "center";
  544. collapsedIndicator.style.justifyContent = "center";
  545. collapsedIndicator.style.transition = "all 0.3s ease";
  546.  
  547. collapsedIndicator.innerHTML = `
  548. <svg width="24" height="24" viewBox="0 0 64 64">
  549. <defs>
  550. <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
  551. <stop offset="0%" style="stop-color:#666;stop-opacity:1" />
  552. <stop offset="100%" style="stop-color:#666;stop-opacity:0.8" />
  553. </linearGradient>
  554. <filter id="glow">
  555. <feGaussianBlur stdDeviation="1" result="coloredBlur"/>
  556. <feMerge>
  557. <feMergeNode in="coloredBlur"/>
  558. <feMergeNode in="SourceGraphic"/>
  559. </feMerge>
  560. </filter>
  561. </defs>
  562. <g id="icon-group" filter="url(#glow)" transform="rotate(165, 32, 32)">
  563. <circle cx="32" cy="32" r="28" fill="url(#gradient)" stroke="#fff" stroke-width="1"/>
  564. <circle cx="32" cy="32" r="20" fill="none" stroke="#fff" stroke-width="1"
  565. stroke-dasharray="80 40" transform="rotate(-90, 32, 32)">
  566. <animate attributeName="stroke-dashoffset"
  567. dur="4s"
  568. values="0;120"
  569. repeatCount="indefinite"
  570. id="outer-ring-anim"/>
  571. </circle>
  572. <circle cx="32" cy="32" r="12" fill="none" stroke="#fff" stroke-width="1">
  573. <animate attributeName="r"
  574. dur="2s"
  575. values="12;14;12"
  576. repeatCount="indefinite"
  577. id="middle-ring-anim"/>
  578. </circle>
  579. <circle id="center-dot" cx="32" cy="32" r="4" fill="#fff">
  580. <animate attributeName="r"
  581. dur="1s"
  582. values="4;5;4"
  583. repeatCount="indefinite"
  584. id="center-dot-anim"/>
  585. </circle>
  586. </g>
  587. </svg>
  588. `;
  589. document.body.appendChild(collapsedIndicator);
  590.  
  591. collapsedIndicator.addEventListener("mouseenter", () => {
  592. displayBox.style.display = "block";
  593. requestAnimationFrame(() => {
  594. displayBox.style.opacity = "1";
  595. displayBox.style.transform = "translateX(0)";
  596. });
  597. });
  598.  
  599. displayBox.addEventListener("mouseleave", () => {
  600. displayBox.style.opacity = "0";
  601. displayBox.style.transform = "translateX(10px)";
  602. setTimeout(() => {
  603. displayBox.style.display = "none";
  604. }, 150);
  605. });
  606.  
  607. const observer = new MutationObserver(updateTheme);
  608. observer.observe(document.documentElement, {
  609. attributes: true,
  610. attributeFilter: ["class"],
  611. });
  612.  
  613. fetchIPInfo();
  614. fetchChatGPTStatus();
  615. updateTheme();
  616. const statusCheckInterval = 60 * 60 * 1000;
  617. let statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);
  618.  
  619. document.addEventListener("visibilitychange", () => {
  620. if (document.visibilityState === "visible") {
  621. clearInterval(statusCheckTimer);
  622. fetchChatGPTStatus();
  623. statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);
  624. }
  625. });
  626. }
  627.  
  628. if (document.readyState !== "loading") {
  629. initUI();
  630. } else {
  631. document.addEventListener("DOMContentLoaded", initUI);
  632. }
  633.  
  634. function maskIP(ip) {
  635. if (!ip || ip === "Unknown") return ip;
  636. if (ip.includes(".")) {
  637. const parts = ip.split(".");
  638. if (parts.length === 4) {
  639. return `${parts[0]}.*.*.${parts[3]}`;
  640. }
  641. }
  642. if (ip.includes(":")) {
  643. const parts = ip.split(":");
  644. // Shorten IPv6 to just show first and last part
  645. if (parts.length > 2) {
  646. return `${parts[0]}:*:${parts[parts.length - 1]}`;
  647. }
  648. }
  649. return ip;
  650. }
  651.  
  652. async function fetchIPQuality(ip) {
  653. try {
  654. const response = await new Promise((resolve, reject) => {
  655. GM_xmlhttpRequest({
  656. method: "GET",
  657. url: `https://scamalytics.com/ip/${ip}`,
  658. timeout: 3000,
  659. onload: (r) =>
  660. r.status === 200
  661. ? resolve(r.responseText)
  662. : reject(new Error(`HTTP ${r.status}`)),
  663. onerror: reject,
  664. ontimeout: () => reject(new Error("Request timed out")),
  665. });
  666. });
  667. const parser = new DOMParser();
  668. const doc = parser.parseFromString(response, "text/html");
  669. const scoreElement = doc.querySelector(".score_bar .score");
  670. const scoreMatch =
  671. scoreElement?.textContent.match(/Fraud Score:\s*(\d+)/i);
  672. if (!scoreMatch) {
  673. return {
  674. label: "Unknown",
  675. color: "#aaa",
  676. tooltip: "Could not determine IP quality",
  677. score: null
  678. };
  679. }
  680. const score = parseInt(scoreMatch[1], 10);
  681. const riskElement = doc.querySelector(".panel_title");
  682. const riskText = riskElement?.textContent.trim() || "Unknown Risk";
  683. const panelColor = riskElement?.style.backgroundColor || "#aaa";
  684. const descriptionElement = doc.querySelector(".panel_body");
  685. const description = descriptionElement?.textContent.trim() || "";
  686. const trimmedDescription =
  687. description.length > 150
  688. ? `${description.substring(0, 147)}...`
  689. : description;
  690.  
  691. function extractTableValue(header) {
  692. const row = Array.from(doc.querySelectorAll("th")).find(
  693. (th) => th.textContent.trim() === header,
  694. )?.parentElement;
  695. return row?.querySelector("td")?.textContent.trim() || null;
  696. }
  697. function isRiskYes(header) {
  698. const row = Array.from(doc.querySelectorAll("th")).find(
  699. (th) => th.textContent.trim() === header,
  700. )?.parentElement;
  701. return row?.querySelector(".risk.yes") !== null;
  702. }
  703. const details = {
  704. location: extractTableValue("City") || "Unknown",
  705. state: extractTableValue("State / Province"),
  706. country: extractTableValue("Country Name"),
  707. isp: extractTableValue("ISP Name") || "Unknown",
  708. organization: extractTableValue("Organization Name"),
  709. isVPN: isRiskYes("Anonymizing VPN"),
  710. isTor: isRiskYes("Tor Exit Node"),
  711. isServer: isRiskYes("Server"),
  712. isProxy:
  713. isRiskYes("Public Proxy") ||
  714. isRiskYes("Web Proxy") ||
  715. isRiskYes("Proxy"),
  716. };
  717. let label, color;
  718. if (riskText && riskText !== "Unknown Risk") {
  719. label = riskText;
  720. color = panelColor !== "#aaa" ? panelColor : getColorForScore(score);
  721. } else {
  722. ({ label, color } = getLabelAndColorForScore(score));
  723. }
  724. const warnings = [];
  725. if (details.isVPN) warnings.push("VPN");
  726. if (details.isTor) warnings.push("Tor");
  727. if (details.isServer) warnings.push("Server");
  728. if (details.isProxy) warnings.push("Proxy");
  729. const location = [details.location, details.state, details.country]
  730. .filter(Boolean)
  731. .join(", ");
  732. const tooltip = [
  733. "IP Risk Info (Scamlytics):",
  734. label !== "Unknown" ? `Risk: ${label} (${score}/100)` : "",
  735. `Location: ${location}`,
  736. `ISP: ${details.isp}${details.organization ? ` (${details.organization})` : ""}`,
  737. warnings.length ? `Warnings: ${warnings.join(", ")}` : "",
  738. trimmedDescription ? `\n${trimmedDescription}` : "",
  739. "\nClick to view full analysis",
  740. ]
  741. .filter(Boolean)
  742. .join("\n");
  743. return { label, color, tooltip, score };
  744. } catch (error) {
  745. return {
  746. label: "Unknown",
  747. color: "#aaa",
  748. tooltip: "Could not check IP quality",
  749. score: null
  750. };
  751. }
  752. }
  753.  
  754. function getColorForScore(score) {
  755. if (score < 25) return "#4CAF50";
  756. if (score < 50) return "#859F3D";
  757. if (score < 75) return "#FAB12F";
  758. return "#e63946";
  759. }
  760.  
  761. function getLabelAndColorForScore(score) {
  762. if (score < 25) return { label: t('riskLevels.veryEasy'), color: "#4CAF50" };
  763. if (score < 50) return { label: t('riskLevels.easy'), color: "#859F3D" };
  764. if (score < 75) return { label: t('riskLevels.medium'), color: "#FAB12F" };
  765. return { label: t('riskLevels.critical'), color: "#e63946" };
  766. }
  767.  
  768. function getIPLogs() {
  769. try {
  770. const logs = localStorage.getItem('chatgpt_ip_logs');
  771. return logs ? JSON.parse(logs) : [];
  772. } catch (error) {
  773. console.error('Error reading IP logs:', error);
  774. return [];
  775. }
  776. }
  777.  
  778. function addIPLog(ip, score, difficulty) {
  779. try {
  780. const logs = getIPLogs();
  781. const timestamp = new Date().toISOString();
  782. const newLog = { timestamp, ip, score, difficulty };
  783. if (logs.length > 0 && logs[0].ip === ip) {
  784. logs[0] = newLog;
  785. } else {
  786. logs.unshift(newLog);
  787. }
  788. const trimmedLogs = logs.slice(0, 10);
  789. localStorage.setItem('chatgpt_ip_logs', JSON.stringify(trimmedLogs));
  790. return trimmedLogs;
  791. } catch (error) {
  792. console.error('Error adding IP log:', error);
  793. return [];
  794. }
  795. }
  796.  
  797. function formatIPLogs(logs) {
  798. return logs.map(log => {
  799. const date = new Date(log.timestamp);
  800. const formattedDate = date.toLocaleString('en-US', {
  801. year: 'numeric',
  802. month: '2-digit',
  803. day: '2-digit',
  804. hour: '2-digit',
  805. minute: '2-digit',
  806. hour12: false
  807. }).replace(/(\d+)\/(\d+)\/(\d+),\s(\d+):(\d+)/, '[$3-$1-$2 $4:$5]');
  808. const { color: powColor, level: powLevel } = getRiskColorAndLevel(log.difficulty);
  809. const scoreDisplay = log.score !== null && log.score !== undefined ? log.score : 'N/A';
  810. return `${formattedDate} ${log.ip}(${scoreDisplay}), ${log.difficulty || 'N/A'}(${powLevel})`;
  811. }).join('\n');
  812. }
  813.  
  814. async function fetchIPInfo() {
  815. try {
  816. const response = await fetch("https://chatgpt.com/cdn-cgi/trace");
  817. const text = await response.text();
  818. const data = text.split("\n").reduce((obj, line) => {
  819. const [key, value] = line.split("=");
  820. if (key && value) obj[key.trim()] = value.trim();
  821. return obj;
  822. }, {});
  823. const ipElement = document.getElementById("ip-address");
  824. const warpBadge = document.getElementById("warp-badge");
  825. const ipQualityElement = document.getElementById("ip-quality");
  826. if (!ipElement || !warpBadge || !ipQualityElement) return;
  827.  
  828. const maskedIP = maskIP(data.ip);
  829. const fullIP = data.ip || "Unknown";
  830. const warpStatus = data.warp || "off";
  831. ipElement.innerText = maskedIP;
  832. ipElement.dataset.fullIp = fullIP;
  833.  
  834. if (warpStatus === "on" || warpStatus === "plus") {
  835. warpBadge.style.display = "inline-flex";
  836. warpBadge.innerText = warpStatus === "plus" ? "warp+" : "warp";
  837. warpBadge.dataset.tooltip = `Protected by Cloudflare WARP${warpStatus === "plus" ? "+" : ""}`;
  838. } else {
  839. warpBadge.style.display = "none";
  840. }
  841.  
  842. const { label, color, tooltip, score } = await fetchIPQuality(fullIP);
  843. ipElement.style.color = color;
  844. ipQualityElement.innerText = score !== null ? `${label} (${score})` : label;
  845. ipQualityElement.style.color = color;
  846. ipQualityElement.dataset.score = score;
  847.  
  848. const difficultyElement = document.getElementById("difficulty");
  849. const currentDifficulty = difficultyElement?.innerText || "N/A";
  850. const logs = addIPLog(fullIP, score, currentDifficulty);
  851. const formattedLogs = formatIPLogs(logs);
  852. const ipContainerTooltip = [
  853. "IP History (recent 10):",
  854. formattedLogs,
  855. "\n---",
  856. "Click to copy full history"
  857. ].join('\n');
  858. ipElement.dataset.tooltip = ipContainerTooltip;
  859. ipQualityElement.dataset.tooltip = tooltip;
  860.  
  861. ipQualityElement.onclick = () =>
  862. window.open(`https://scamalytics.com/ip/${fullIP}`, "_blank");
  863.  
  864. const copyHandler = async () => {
  865. try {
  866. const logs = getIPLogs();
  867. const formattedHistory = formatIPLogs(logs);
  868. await navigator.clipboard.writeText(formattedHistory);
  869. const originalText = ipElement.innerText;
  870. ipElement.innerText = "History copied!";
  871. setTimeout(() => {
  872. ipElement.innerText = originalText;
  873. }, 1000);
  874. } catch (err) {
  875. ipElement.innerText = "Copy failed";
  876. setTimeout(() => {
  877. ipElement.innerText = maskedIP;
  878. }, 1000);
  879. }
  880. };
  881. ipElement.removeEventListener("click", copyHandler);
  882. ipElement.addEventListener("click", copyHandler);
  883. } catch (error) {
  884. const ipElement = document.getElementById("ip-address");
  885. const warpBadge = document.getElementById("warp-badge");
  886. const ipQualityElement = document.getElementById("ip-quality");
  887. if (ipElement) ipElement.innerText = "Failed to fetch";
  888. if (warpBadge) warpBadge.style.display = "none";
  889. if (ipQualityElement) {
  890. ipQualityElement.innerText = "Unknown";
  891. ipQualityElement.style.color = "#aaa";
  892. ipQualityElement.dataset.tooltip = "Could not check IP quality";
  893. }
  894. }
  895. }
  896.  
  897. async function fetchChatGPTStatus() {
  898. try {
  899. if (typeof GM_xmlhttpRequest === "undefined") {
  900. throw new Error("GM_xmlhttpRequest not supported");
  901. }
  902. return new Promise((resolve, reject) => {
  903. GM_xmlhttpRequest({
  904. method: "GET",
  905. url: "https://status.openai.com/api/v2/status.json",
  906. timeout: 3000,
  907. ontimeout: () => reject(new Error("Status check timed out")),
  908. onload: (response) => {
  909. if (response.status === 200) {
  910. try {
  911. const data = JSON.parse(response.responseText);
  912. const status = data.status;
  913. const statusDescription =
  914. document.getElementById("status-description");
  915. const statusMonitorItem =
  916. statusDescription?.closest(".monitor-item");
  917. if (!statusDescription || !statusMonitorItem) {
  918. reject(new Error("Status UI elements not found"));
  919. return;
  920. }
  921. statusMonitorItem.style.display = "block";
  922. if (status) {
  923. const indicator = (status.indicator || "").toLowerCase();
  924. const description =
  925. status.description || "All Systems Operational";
  926. const indicatorColors = {
  927. none: "var(--success-color, #10a37f)",
  928. minor: "#FAB12F",
  929. major: "#FFA500",
  930. critical: "#e63946",
  931. };
  932. if (description === "All Systems Operational") {
  933. statusDescription.style.color =
  934. "var(--success-color, #10a37f)";
  935. } else {
  936. statusDescription.style.color =
  937. indicatorColors[indicator] || "#aaa";
  938. }
  939. statusDescription.textContent = description;
  940. }
  941. resolve();
  942. } catch (err) {
  943. reject(err);
  944. }
  945. } else {
  946. reject(new Error(`HTTP error: ${response.status}`));
  947. }
  948. },
  949. onerror: (err) => reject(err),
  950. });
  951. });
  952. } catch (error) {
  953. const statusDescription = document.getElementById("status-description");
  954. const statusMonitorItem = statusDescription?.closest(".monitor-item");
  955. if (statusMonitorItem) statusMonitorItem.style.display = "none";
  956. }
  957. }
  958.  
  959. function updateTheme() {
  960. const isDark =
  961. document.documentElement.classList.contains("dark") ||
  962. localStorage.getItem("theme") === "dark" ||
  963. document.documentElement.dataset.theme === "dark";
  964. displayBox.style.backgroundColor = isDark
  965. ? "var(--surface-primary, rgba(0, 0, 0, 0.8))"
  966. : "var(--surface-primary, rgba(255, 255, 255, 0.9))";
  967. displayBox.style.color = isDark
  968. ? "var(--text-primary, #fff)"
  969. : "var(--text-primary, #000)";
  970. displayBox.querySelectorAll(".label").forEach((label) => {
  971. label.style.color = isDark
  972. ? "var(--text-secondary, #aaa)"
  973. : "var(--text-secondary, #666)";
  974. });
  975. }
  976. })();

QingJ © 2025

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