// ==UserScript== // @name ChatGPT Reset Expiry|ChatGPT 重置额度有效期查询插件 // @name:en ChatGPT Reset Expiry // @name:zh ChatGPT 重置额度有效期查询插件 // @name:zh-CN ChatGPT 重置额度有效期查询插件 // @name:zh-HK ChatGPT 重置額度有效期查詢插件 // @name:zh-MO ChatGPT 重置額度有效期查詢插件 // @name:zh-TW ChatGPT 重置額度有效期查詢插件 // @name:ja ChatGPT リセットクレジット有効期限確認ツール // @name:ko ChatGPT 리셋 크레딧 만료 시간 확인 도구 // @name:es ChatGPT Reset Expiry|Consulta de caducidad de créditos // @name:de ChatGPT Reset Expiry|Ablaufzeit der Reset-Credits // @name:fr ChatGPT Reset Expiry|Vérification de l’expiration des crédits // @namespace https://github.com/kadto183/chatgpt-reset-expiry // @version 1.16 // @description View the exact expiration time of ChatGPT reset credits in your browser's local timezone.|查询 ChatGPT 重置额度的准确有效期与失效时间,并自动转换为浏览器当前时区。 // @description:en View the exact expiration time of ChatGPT reset credits in your browser's local timezone. // @description:zh 查询 ChatGPT 重置额度的准确有效期与失效时间,精确到秒并自动转换为浏览器当前时区。 // @description:zh-CN 查询 ChatGPT 重置额度的准确有效期与失效时间,精确到秒并自动转换为浏览器当前时区。 // @description:zh-HK 查詢 ChatGPT 重置額度的準確有效期與失效時間,精確到秒並自動轉換為瀏覽器目前時區。 // @description:zh-MO 查詢 ChatGPT 重置額度的準確有效期與失效時間,精確到秒並自動轉換為瀏覽器目前時區。 // @description:zh-TW 查詢 ChatGPT 重置額度的準確有效期與失效時間,精確到秒並自動轉換為瀏覽器目前時區。 // @description:ja ChatGPT のリセットクレジットの正確な有効期限を秒単位で確認し、ブラウザのローカルタイムゾーンで表示します。 // @description:ko ChatGPT 리셋 크레딧의 정확한 만료 시간을 초 단위로 확인하고 브라우저의 현지 시간대로 표시합니다. // @description:es Consulta la hora exacta de caducidad de los créditos de restablecimiento de ChatGPT y conviértela a la zona horaria local del navegador. // @description:de Zeigt die genaue Ablaufzeit der ChatGPT Reset-Credits an und rechnet sie in die lokale Browser-Zeitzone um. // @description:fr Affiche l’heure exacte d’expiration des crédits de réinitialisation ChatGPT et la convertit dans le fuseau horaire local du navigateur. // @author kadto183 // @license MIT // @homepageURL https://github.com/kadto183/chatgpt-reset-expiry // @supportURL https://github.com/kadto183/chatgpt-reset-expiry/issues // @downloadURL https://raw.githubusercontent.com/kadto183/chatgpt-reset-expiry/main/chatgpt-reset-expiry.user.js // @updateURL https://raw.githubusercontent.com/kadto183/chatgpt-reset-expiry/main/chatgpt-reset-expiry.user.js // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @run-at document-start // @grant none // ==/UserScript== (function () { 'use strict'; const PREFIX = '[ChatGPT Reset Expiry]'; // --------------------------------------------------------------------- // Data source // --------------------------------------------------------------------- const RESET_CREDITS_URL = '/backend-api/wham/rate-limit-reset-credits'; const CANDIDATE_URL_RE = /(usage|reset|credit|limit|subscription|plan|wham|rate)/i; const RELATED_KEY_RE = /(expire|expires|expiry|expiration|reset|credit|limit|valid_until)/i; const HIGH_VALUE_KEY_RE = /(expires?_?at|expiry|expiration|reset_?at|reset_?time|valid_?until)/i; // --------------------------------------------------------------------- // Browser timezone and UI language // --------------------------------------------------------------------- const LOCAL_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; const SIMPLIFIED_CHINESE_TIME_ZONES = new Set([ 'Asia/Shanghai', 'Asia/Chongqing', 'Asia/Chungking', 'Asia/Harbin', 'Asia/Urumqi', 'Asia/Kashgar' ]); const TRADITIONAL_CHINESE_TIME_ZONE_LOCALES = { 'Asia/Hong_Kong': 'zh-HK', 'Asia/Macau': 'zh-MO', 'Asia/Taipei': 'zh-TW' }; const IS_SIMPLIFIED_CHINESE_ZONE = SIMPLIFIED_CHINESE_TIME_ZONES.has(LOCAL_TIME_ZONE); const IS_TRADITIONAL_CHINESE_ZONE = Object.prototype.hasOwnProperty.call( TRADITIONAL_CHINESE_TIME_ZONE_LOCALES, LOCAL_TIME_ZONE ); const UI_LANG = IS_SIMPLIFIED_CHINESE_ZONE ? 'zh-Hans' : IS_TRADITIONAL_CHINESE_ZONE ? 'zh-Hant' : 'en'; const UI_LOCALE = IS_SIMPLIFIED_CHINESE_ZONE ? 'zh-CN' : TRADITIONAL_CHINESE_TIME_ZONE_LOCALES[ LOCAL_TIME_ZONE ] || 'en-US'; const CHINESE_ZONE_NAMES = { 'Asia/Shanghai': '中国标准时间', 'Asia/Chongqing': '中国标准时间', 'Asia/Chungking': '中国标准时间', 'Asia/Harbin': '中国标准时间', 'Asia/Urumqi': '中国乌鲁木齐时间', 'Asia/Kashgar': '中国喀什时间', 'Asia/Hong_Kong': '中國香港時間', 'Asia/Macau': '中國澳門時間', 'Asia/Taipei': '中國台灣時間' }; // --------------------------------------------------------------------- // Localized UI text // --------------------------------------------------------------------- const TEXT = { 'zh-Hans': { title: 'GPT 重置额度有效期', subtitle: '查询额度具体到几点失效', more: '更多信息', waiting: '正在检测当前页面…', found: count => `当前有效额度 ${count} 个`, empty: '当前尚未检测到重置额度', noActive: '当前没有有效的重置额度', history: '已失效额度', dataReady: '✓ 已从 ChatGPT 页面数据读取', dataWaiting: '等待 ChatGPT 页面数据', credit: index => `重置额度 ${index}`, nearest: '最近到期', expired: '已失效', copy: '复制全部时间', copied: '已复制', other: '其他检测结果', noOther: '暂无其他检测结果', source: '数据来源', timezone: '当前时区', toggle: '展开或收起', close: '关闭' }, 'zh-Hant': { title: 'GPT 重置額度有效期', subtitle: '查詢額度具體到幾點失效', more: '更多資訊', waiting: '正在偵測目前頁面…', found: count => `目前有效額度 ${count} 個`, empty: '目前尚未偵測到重置額度', noActive: '目前沒有有效的重置額度', history: '已失效額度', dataReady: '✓ 已從 ChatGPT 頁面資料讀取', dataWaiting: '等待 ChatGPT 頁面資料', credit: index => `重置額度 ${index}`, nearest: '最近到期', expired: '已失效', copy: '複製全部時間', copied: '已複製', other: '其他偵測結果', noOther: '暫無其他偵測結果', source: '資料來源', timezone: '目前時區', toggle: '展開或收起', close: '關閉' }, en: { title: 'GPT Reset Expiry', subtitle: 'View the exact expiration time', more: 'More', waiting: 'Checking the current page…', found: count => `${count} active reset credit${count === 1 ? '' : 's'}`, empty: 'No reset credits detected on this page', noActive: 'No active reset credits', history: 'Expired Credits', dataReady: '✓ Read from ChatGPT page data', dataWaiting: 'Waiting for ChatGPT page data', credit: index => `Reset Credit ${index}`, nearest: 'Expires Next', expired: 'Expired', copy: 'Copy All Times', copied: 'Copied', other: 'Other Detected Results', noOther: 'No other results', source: 'Data source', timezone: 'Current time zone', toggle: 'Expand or collapse', close: 'Close' } }; const T = TEXT[UI_LANG]; // --------------------------------------------------------------------- // Runtime state // --------------------------------------------------------------------- const resetCredits = new Map(); const otherMatches = new Map(); let panel = null; let mainList = null; let otherList = null; let statusEl = null; const STORAGE_KEYS = { panelPreference: 'chatgpt-reset-expiry:panel-preference', panelPosition: 'chatgpt-reset-expiry:panel-position' }; function readStorage(key) { try { return localStorage.getItem(key); } catch (_) { return null; } } function writeStorage(key, value) { try { localStorage.setItem(key, value); } catch (_) {} } // Always start quietly. Saved preferences are applied after confirmed // reset-credit data is detected. const savedPanelPreference = readStorage(STORAGE_KEYS.panelPreference); let isExpanded = false; let userCollapsed = savedPanelPreference === 'collapsed'; let autoExpandedOnce = false; let dismissed = false; let suppressHeaderClick = false; // --------------------------------------------------------------------- // Time helpers // --------------------------------------------------------------------- function parseDate(value) { if (value === null || value === undefined) { return null; } let date = null; if (typeof value === 'number') { if (value > 1e9 && value < 1e11) { date = new Date(value * 1000); } else if (value >= 1e11 && value < 1e15) { date = new Date(value); } } if (typeof value === 'string') { const text = value.trim(); if (/^\d{10}$/.test(text)) { date = new Date(Number(text) * 1000); } else if (/^\d{13}$/.test(text)) { date = new Date(Number(text)); } else { const timestamp = Date.parse(text); if (!Number.isNaN(timestamp)) { date = new Date(timestamp); } } } if (!date || Number.isNaN(date.getTime())) { return null; } const year = date.getUTCFullYear(); if (year < 2020 || year > 2100) { return null; } return date; } function getDateParts(date) { const parts = new Intl.DateTimeFormat( 'en-CA', { timeZone: LOCAL_TIME_ZONE, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' } ).formatToParts(date); const result = {}; for (const part of parts) { if (part.type !== 'literal') { result[part.type] = part.value; } } return result; } function formatDateOnly(date) { const p = getDateParts(date); return `${p.year}/${p.month}/${p.day}`; } function formatTimeOnly(date) { const p = getDateParts(date); return `${p.hour}:${p.minute}:${p.second}`; } function formatLocal(date) { return `${formatDateOnly(date)} ${formatTimeOnly(date)}`; } function getUtcOffset(date) { try { const parts = new Intl.DateTimeFormat( 'en-US', { timeZone: LOCAL_TIME_ZONE, timeZoneName: 'longOffset' } ).formatToParts(date); const zone = parts.find( part => part.type === 'timeZoneName' )?.value; if (zone) { return zone.replace('GMT', 'UTC'); } } catch (_) {} // LOCAL_TIME_ZONE comes from the browser/system, so this fallback // remains correct for the browser's active local timezone. const offsetMinutes = -date.getTimezoneOffset(); const sign = offsetMinutes >= 0 ? '+' : '-'; const absolute = Math.abs(offsetMinutes); const hours = String( Math.floor(absolute / 60) ).padStart(2, '0'); const minutes = String( absolute % 60 ).padStart(2, '0'); return `UTC${sign}${hours}:${minutes}`; } function getTimeZoneDisplayName(date) { if (CHINESE_ZONE_NAMES[LOCAL_TIME_ZONE]) { return CHINESE_ZONE_NAMES[LOCAL_TIME_ZONE]; } try { const parts = new Intl.DateTimeFormat( UI_LOCALE, { timeZone: LOCAL_TIME_ZONE, timeZoneName: 'long' } ).formatToParts(date); const zoneName = parts.find( part => part.type === 'timeZoneName' )?.value; if (zoneName) { return zoneName; } } catch (_) {} return LOCAL_TIME_ZONE; } function formatTimeZoneLabel(date) { return ( `${getTimeZoneDisplayName(date)} · ` + `${getUtcOffset(date)}` ); } function getCurrentZoneSummary() { return formatTimeZoneLabel(new Date()); } // --------------------------------------------------------------------- // DOM helpers // --------------------------------------------------------------------- function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function applyExpandedState() { if (!panel) { return; } panel.classList.toggle( 'grf-expanded', isExpanded ); panel.classList.toggle( 'grf-collapsed', !isExpanded ); const toggleButton = panel.querySelector('#grf-toggle'); if (toggleButton) { toggleButton.textContent = isExpanded ? '−' : '+'; } } function setExpanded(expanded, byUser = false) { isExpanded = expanded; if (byUser) { userCollapsed = !expanded; writeStorage( STORAGE_KEYS.panelPreference, expanded ? 'expanded' : 'collapsed' ); } applyExpandedState(); } function maybeAutoExpand() { if ( dismissed || autoExpandedOnce || resetCredits.size === 0 ) { return; } autoExpandedOnce = true; if ( savedPanelPreference === 'collapsed' || userCollapsed ) { return; } setExpanded(true, false); } function detectDarkTheme() { const root = document.documentElement; const body = document.body; const explicitTheme = root?.getAttribute('data-theme') || body?.getAttribute?.('data-theme'); if (explicitTheme === 'dark') return true; if (explicitTheme === 'light') return false; if ( root?.classList.contains('dark') || body?.classList?.contains('dark') ) { return true; } if ( root?.classList.contains('light') || body?.classList?.contains('light') ) { return false; } return window.matchMedia?.( '(prefers-color-scheme: dark)' )?.matches ?? true; } function applyTheme() { if (!panel) return; const dark = detectDarkTheme(); panel.classList.toggle( 'grf-dark', dark ); panel.classList.toggle( 'grf-light', !dark ); } function installThemeWatcher() { const observer = new MutationObserver(applyTheme); observer.observe( document.documentElement, { attributes: true, attributeFilter: [ 'class', 'data-theme', 'style' ] } ); const media = window.matchMedia?.( '(prefers-color-scheme: dark)' ); if (media?.addEventListener) { media.addEventListener( 'change', applyTheme ); } } function clampPanelPosition(left, top) { const width = panel?.offsetWidth || 174; const height = panel?.offsetHeight || 44; const margin = 8; return { left: Math.min( Math.max(margin, left), Math.max( margin, window.innerWidth - width - margin ) ), top: Math.min( Math.max(margin, top), Math.max( margin, window.innerHeight - height - margin ) ) }; } function restorePanelPosition() { const raw = readStorage( STORAGE_KEYS.panelPosition ); if (!raw || !panel) return; try { const saved = JSON.parse(raw); if ( !Number.isFinite(saved.left) || !Number.isFinite(saved.top) ) { return; } const pos = clampPanelPosition( saved.left, saved.top ); panel.style.left = `${pos.left}px`; panel.style.top = `${pos.top}px`; panel.style.right = 'auto'; } catch (_) {} } function savePanelPosition() { if (!panel) return; const rect = panel.getBoundingClientRect(); writeStorage( STORAGE_KEYS.panelPosition, JSON.stringify({ left: Math.round(rect.left), top: Math.round(rect.top) }) ); } function installDragBehaviour() { if (!panel) return; const header = panel.querySelector('#grf-header'); if (!header) return; let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; let moved = false; header.addEventListener( 'pointerdown', event => { if ( event.button !== 0 || event.target.closest( '#grf-header-actions' ) ) { return; } const rect = panel.getBoundingClientRect(); dragging = true; moved = false; startX = event.clientX; startY = event.clientY; startLeft = rect.left; startTop = rect.top; header.setPointerCapture?.( event.pointerId ); } ); header.addEventListener( 'pointermove', event => { if (!dragging) return; const dx = event.clientX - startX; const dy = event.clientY - startY; if ( Math.abs(dx) > 4 || Math.abs(dy) > 4 ) { moved = true; } if (!moved) return; const pos = clampPanelPosition( startLeft + dx, startTop + dy ); panel.style.left = `${pos.left}px`; panel.style.top = `${pos.top}px`; panel.style.right = 'auto'; event.preventDefault(); } ); const endDrag = event => { if (!dragging) return; dragging = false; if (moved) { suppressHeaderClick = true; savePanelPosition(); setTimeout( () => { suppressHeaderClick = false; }, 0 ); } try { header.releasePointerCapture?.( event.pointerId ); } catch (_) {} }; header.addEventListener( 'pointerup', endDrag ); header.addEventListener( 'pointercancel', endDrag ); window.addEventListener( 'resize', () => { if (!panel) return; const rect = panel.getBoundingClientRect(); const pos = clampPanelPosition( rect.left, rect.top ); panel.style.left = `${pos.left}px`; panel.style.top = `${pos.top}px`; panel.style.right = 'auto'; savePanelPosition(); } ); } // --------------------------------------------------------------------- // UI // --------------------------------------------------------------------- function createPanel() { if ( panel || !document.documentElement ) { return; } const style = document.createElement('style'); style.textContent = ` #gpt-reset-finder-panel { position: fixed; top: 76px; right: 20px; z-index: 2147483647; color: #f0f0f0; background: rgba(24,24,24,.97); border: 1px solid rgba(255,255,255,.11); border-radius: 15px; box-shadow: 0 16px 45px rgba(0,0,0,.35); font-family: "Microsoft YaHei", "Segoe UI", system-ui, -apple-system, sans-serif; overflow: hidden; backdrop-filter: blur(14px); transition: width .18s ease, border-radius .18s ease; } #gpt-reset-finder-panel.grf-collapsed { width: 158px; border-radius: 12px; } #gpt-reset-finder-panel.grf-collapsed #grf-body { display: none; } #gpt-reset-finder-panel.grf-collapsed #grf-header { padding: 9px 9px 9px 11px; border-bottom: 0; cursor: pointer; } #gpt-reset-finder-panel.grf-collapsed #grf-title { font-size: 12px; } #gpt-reset-finder-panel.grf-collapsed #grf-subtitle { display: none; } #gpt-reset-finder-panel.grf-collapsed #grf-close { display: none; } #gpt-reset-finder-panel.grf-expanded { width: 360px; max-height: 70vh; } #grf-header { display: flex; cursor: move; touch-action: none; align-items: center; justify-content: space-between; padding: 11px 12px 10px; border-bottom: 1px solid rgba(255,255,255,.075); background: linear-gradient( 180deg, rgba(255,255,255,.038), rgba(255,255,255,0) ); user-select: none; } #grf-title-wrap { display: flex; flex-direction: column; gap: 0; } #grf-title-line { display: flex; align-items: center; gap: 7px; } #grf-title { font-size: 14px; font-weight: 650; letter-spacing: .1px; } #grf-mini-count { min-width: 17px; height: 17px; display: none; align-items: center; justify-content: center; padding: 0 5px; border-radius: 999px; background: rgba(236,188,72,.14); border: 1px solid rgba(236,188,72,.24); color: #e5c467; font-size: 9px; } #grf-mini-count.grf-visible { display: inline-flex; } #grf-subtitle { display: none; } #grf-header-actions { display: flex; gap: 4px; } .grf-icon-btn { width: 29px; height: 29px; border: 0; border-radius: 7px; background: transparent; color: #999; cursor: pointer; font-size: 16px; } .grf-icon-btn:hover { background: rgba(255,255,255,.075); color: #eee; } #grf-body { max-height: calc(70vh - 52px); overflow-y: auto; } #grf-overview { padding: 9px 12px 2px; } #grf-status { font-size: 12px; color: #999; } #grf-current-zone { display: none; } #grf-main-list { padding: 3px 10px 8px; } .grf-card { position: relative; margin-top: 7px; padding: 10px 11px 11px; background: linear-gradient( 145deg, rgba(255,255,255,.047), rgba(255,255,255,.018) ); border: 1px solid rgba(255,255,255,.095); border-radius: 11px; } .grf-card:hover { border-color: rgba(255,255,255,.16); } .grf-card.grf-nearest { border-color: rgba(236,188,72,.48); background: linear-gradient( 145deg, rgba(236,188,72,.085), rgba(255,255,255,.018) ); box-shadow: inset 2px 0 0 rgba(236,188,72,.72); } .grf-card.grf-expired { opacity: .48; } .grf-card-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 7px; } .grf-card-name { font-size: 11px; color: #a8a8a8; } .grf-badge, .grf-expired-label { padding: 2px 7px; border-radius: 999px; font-size: 10px; } .grf-badge { background: rgba(236,188,72,.12); border: 1px solid rgba(236,188,72,.25); color: #e5c467; } .grf-expired-label { background: rgba(255,255,255,.035); border: 1px solid rgba(255,255,255,.07); color: #888; } .grf-date-row { display: flex; align-items: baseline; gap: 9px; white-space: nowrap; } .grf-date { font-size: 16px; font-weight: 650; color: #f1f1f1; font-variant-numeric: tabular-nums; } .grf-time { font-size: 16px; font-weight: 650; color: #efc969; font-family: "SFMono-Regular", Consolas, monospace; font-variant-numeric: tabular-nums; } .grf-zone { margin-top: 4px; color: #777; font-size: 9.5px; } #grf-actions { display: flex; padding: 1px 10px 9px; } .grf-action-btn { flex: 1; width: 100%; padding: 7px 9px; border-radius: 7px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.043); color: #ccc; cursor: pointer; font-size: 12px; } .grf-action-btn:hover { background: rgba(255,255,255,.075); color: #f2f2f2; } #grf-other-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-top: 1px solid rgba(255,255,255,.07); color: #808080; font-size: 11px; cursor: pointer; user-select: none; } #grf-other-header:hover { background: rgba(255,255,255,.025); } #grf-other-list { display: none; padding: 0 14px 12px; max-height: 220px; overflow-y: auto; } .grf-other-item { padding: 8px 0; border-top: 1px solid rgba(255,255,255,.055); color: #858585; font-size: 10px; line-height: 1.55; word-break: break-all; } .grf-other-key { color: #7fa9d8; } .grf-empty { padding: 19px 10px; text-align: center; color: #727272; font-size: 12px; } #grf-more-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-top: 1px solid rgba(255,255,255,.07); color: #777; font-size: 10.5px; cursor: pointer; user-select: none; } #grf-more-header:hover { background: rgba(255,255,255,.025); } #grf-more-list { display: none; } #grf-history-header { display: none; align-items: center; justify-content: space-between; padding: 10px 14px; border-top: 1px solid rgba(255,255,255,.07); color: #808080; font-size: 11px; cursor: pointer; user-select: none; } #grf-history-header.grf-visible { display: flex; } #grf-more-list #grf-history-header, #grf-more-list #grf-other-header { padding: 7px 12px; border-top: 0; font-size: 10px; background: transparent; } #grf-history-header:hover { background: rgba(255,255,255,.025); } #grf-history-list { display: none; padding: 0 12px 10px; } #grf-history-list .grf-card { opacity: .55; } #grf-footer { padding: 0 14px 12px; color: #666; font-size: 9.5px; line-height: 1.5; word-break: break-all; } #gpt-reset-finder-panel.grf-light { color: #202123; background: rgba(255,255,255,.98); border-color: rgba(0,0,0,.11); box-shadow: 0 16px 45px rgba(0,0,0,.16); } #gpt-reset-finder-panel.grf-light #grf-header { border-bottom-color: rgba(0,0,0,.08); background: linear-gradient( 180deg, rgba(0,0,0,.025), rgba(0,0,0,0) ); } #gpt-reset-finder-panel.grf-light #grf-title, #gpt-reset-finder-panel.grf-light .grf-date { color: #202123; } #gpt-reset-finder-panel.grf-light #grf-subtitle, #gpt-reset-finder-panel.grf-light #grf-status, #gpt-reset-finder-panel.grf-light .grf-card-name { color: #5f6368; } #gpt-reset-finder-panel.grf-light #grf-current-zone, #gpt-reset-finder-panel.grf-light .grf-zone, #gpt-reset-finder-panel.grf-light #grf-footer { color: #777; } #gpt-reset-finder-panel.grf-light .grf-card { background: linear-gradient( 145deg, rgba(0,0,0,.025), rgba(0,0,0,.012) ); border-color: rgba(0,0,0,.10); } #gpt-reset-finder-panel.grf-light .grf-card.grf-nearest { border-color: rgba(180,130,0,.45); background: linear-gradient( 145deg, rgba(236,188,72,.12), rgba(0,0,0,.01) ); } #gpt-reset-finder-panel.grf-light .grf-time { color: #9a6700; } #gpt-reset-finder-panel.grf-light .grf-icon-btn { color: #666; } #gpt-reset-finder-panel.grf-light .grf-icon-btn:hover, #gpt-reset-finder-panel.grf-light .grf-action-btn:hover { background: rgba(0,0,0,.06); color: #202123; } #gpt-reset-finder-panel.grf-light .grf-action-btn { border-color: rgba(0,0,0,.10); background: rgba(0,0,0,.025); color: #444; } #gpt-reset-finder-panel.grf-light #grf-more-header, #gpt-reset-finder-panel.grf-light #grf-other-header, #gpt-reset-finder-panel.grf-light #grf-history-header { border-top-color: rgba(0,0,0,.07); color: #666; } #gpt-reset-finder-panel.grf-light #grf-more-header:hover, #gpt-reset-finder-panel.grf-light #grf-other-header:hover, #gpt-reset-finder-panel.grf-light #grf-history-header:hover { background: rgba(0,0,0,.025); } #gpt-reset-finder-panel.grf-light .grf-other-item { border-top-color: rgba(0,0,0,.06); color: #666; } #gpt-reset-finder-panel.grf-light .grf-empty { color: #777; } `; document.documentElement.appendChild(style); panel = document.createElement('div'); panel.id = 'gpt-reset-finder-panel'; panel.className = 'grf-collapsed'; panel.innerHTML = `
${escapeHtml(T.title)}
0
${escapeHtml(T.subtitle)}
${escapeHtml(T.waiting)}
${escapeHtml(T.empty)}
${escapeHtml(T.more)} ▾
${escapeHtml(T.history)} 0 ▾
${escapeHtml(T.other)} 0 ▾
`; document.documentElement.appendChild(panel); mainList = panel.querySelector('#grf-main-list'); otherList = panel.querySelector('#grf-other-list'); statusEl = panel.querySelector('#grf-status'); applyTheme(); installThemeWatcher(); restorePanelPosition(); installDragBehaviour(); panel .querySelector('#grf-header') .addEventListener( 'click', event => { if ( suppressHeaderClick || event.target.closest('#grf-close') || event.target.closest('#grf-toggle') ) { return; } setExpanded(!isExpanded, true); } ); panel .querySelector('#grf-toggle') .onclick = event => { event.stopPropagation(); setExpanded(!isExpanded, true); }; panel .querySelector('#grf-close') .onclick = event => { event.stopPropagation(); dismissed = true; panel.style.display = 'none'; }; panel .querySelector('#grf-more-header') .onclick = () => { const moreList = panel.querySelector('#grf-more-list'); const isOpen = moreList.style.display === 'block'; moreList.style.display = isOpen ? 'none' : 'block'; const arrow = panel.querySelector('#grf-more-arrow'); arrow.textContent = isOpen ? '▾' : '▴'; }; panel .querySelector('#grf-history-header') .onclick = () => { const historyList = panel.querySelector('#grf-history-list'); const isOpen = historyList.style.display === 'block'; historyList.style.display = isOpen ? 'none' : 'block'; const arrow = panel.querySelector('#grf-history-arrow'); arrow.textContent = isOpen ? '▾' : '▴'; }; panel .querySelector('#grf-other-header') .onclick = () => { const isOpen = otherList.style.display === 'block'; otherList.style.display = isOpen ? 'none' : 'block'; const arrow = panel.querySelector('#grf-other-arrow'); arrow.textContent = isOpen ? '▾' : '▴'; }; panel .querySelector('#grf-copy') .onclick = copyResetTimes; applyExpandedState(); } function ensurePanel() { if (panel) { return; } if (document.documentElement) { createPanel(); } else { document.addEventListener( 'DOMContentLoaded', createPanel, { once: true } ); } } // --------------------------------------------------------------------- // Reset-credit extraction // --------------------------------------------------------------------- function extractResetCredits(obj, url) { if ( !obj || typeof obj !== 'object' ) { return; } function walk(value, path = 'root') { if ( !value || typeof value !== 'object' ) { return; } if (Array.isArray(value)) { value.forEach( (item, index) => { if ( item && typeof item === 'object' && 'expires_at' in item ) { const date = parseDate(item.expires_at); if (date) { const uniqueKey = date.toISOString(); resetCredits.set( uniqueKey, { date, raw: item.expires_at, path: `${path}[${index}].expires_at`, parent: item, url } ); } } walk( item, `${path}[${index}]` ); } ); return; } for ( const [key, child] of Object.entries(value) ) { walk( child, `${path}.${key}` ); } } walk(obj); render(); // Secondary candidate fields never trigger this. Only confirmed // reset-credit data from the dedicated endpoint may auto-open. maybeAutoExpand(); } // --------------------------------------------------------------------- // Secondary candidate scan // --------------------------------------------------------------------- function scanOther( value, url, path = 'root', depth = 0 ) { if ( depth > 20 || value === null || value === undefined ) { return; } if (Array.isArray(value)) { value.forEach( (item, index) => { scanOther( item, url, `${path}[${index}]`, depth + 1 ); } ); return; } if (typeof value !== 'object') { return; } for ( const [key, child] of Object.entries(value) ) { const currentPath = `${path}.${key}`; if (HIGH_VALUE_KEY_RE.test(key)) { const date = parseDate(child); if (date) { const id = `${url}|${currentPath}|${date.toISOString()}`; otherMatches.set( id, { path: currentPath, raw: child, date, url } ); } } scanOther( child, url, currentPath, depth + 1 ); } render(); } // --------------------------------------------------------------------- // Rendering // --------------------------------------------------------------------- function getSortedCredits() { return Array .from(resetCredits.values()) .sort( (a, b) => a.date.getTime() - b.date.getTime() ); } function renderCreditCard( item, index, nearest = null, expired = false ) { const isNearest = item === nearest; return `
${escapeHtml(T.credit(index + 1))} ${ isNearest ? ` ${escapeHtml(T.nearest)} ` : expired ? ` ${escapeHtml(T.expired)} ` : '' }
${escapeHtml( formatDateOnly(item.date) )} ${escapeHtml( formatTimeOnly(item.date) )}
${escapeHtml( formatTimeZoneLabel(item.date) )}
`; } function render() { ensurePanel(); if ( !mainList || !otherList ) { setTimeout(render, 50); return; } const credits = getSortedCredits(); const now = Date.now(); const activeCredits = credits.filter( item => item.date.getTime() > now ); const expiredCredits = credits.filter( item => item.date.getTime() <= now ); const nearest = activeCredits.length ? activeCredits[0] : null; const miniCount = panel.querySelector('#grf-mini-count'); miniCount.textContent = activeCredits.length; miniCount.classList.toggle( 'grf-visible', activeCredits.length > 0 ); if (!credits.length) { statusEl.textContent = T.waiting; mainList.innerHTML = `
${escapeHtml(T.empty)}
`; } else if (!activeCredits.length) { statusEl.textContent = T.noActive; mainList.innerHTML = `
${escapeHtml(T.noActive)}
`; } else { statusEl.textContent = T.found(activeCredits.length); mainList.innerHTML = activeCredits .map( (item, index) => renderCreditCard( item, index, nearest, false ) ) .join(''); } const historyHeader = panel.querySelector('#grf-history-header'); const historyList = panel.querySelector('#grf-history-list'); const historyCount = panel.querySelector('#grf-history-count'); historyCount.textContent = expiredCredits.length; historyHeader.classList.toggle( 'grf-visible', expiredCredits.length > 0 ); if (!expiredCredits.length) { historyList.style.display = 'none'; historyList.innerHTML = ''; } else { historyList.innerHTML = expiredCredits .map( (item, index) => renderCreditCard( item, index, null, true ) ) .join(''); } const others = Array.from(otherMatches.values()); const moreHeader = panel.querySelector('#grf-more-header'); const hasMore = expiredCredits.length > 0 || others.length > 0; moreHeader.style.display = hasMore ? 'flex' : 'none'; if (!hasMore) { const moreList = panel.querySelector('#grf-more-list'); moreList.style.display = 'none'; } panel .querySelector('#grf-other-count') .textContent = others.length; otherList.innerHTML = others.length ? others .map( item => `
${escapeHtml(item.path)}
${escapeHtml( formatLocal(item.date) )}
${escapeHtml( formatTimeZoneLabel(item.date) )}
${escapeHtml(item.url)}
` ) .join('') : `
${escapeHtml(T.noOther)}
`; } // --------------------------------------------------------------------- // Clipboard // --------------------------------------------------------------------- async function copyResetTimes() { const credits = getSortedCredits() .filter( item => item.date.getTime() > Date.now() ); if (!credits.length) { return; } const text = credits .map( (item, index) => `${T.credit(index + 1)}: ` + `${formatLocal(item.date)} ` + `(${formatTimeZoneLabel(item.date)})` ) .join('\n'); try { await navigator.clipboard .writeText(text); const button = panel.querySelector('#grf-copy'); const originalText = button.textContent; button.textContent = T.copied; setTimeout( () => { button.textContent = originalText; }, 1200 ); } catch (_) { console.log( PREFIX, '\n' + text ); } } // --------------------------------------------------------------------- // Response inspection // --------------------------------------------------------------------- function inspectResponse(text, url) { if (!text) { return; } let obj; try { obj = JSON.parse(text); } catch (_) { return; } if (url.includes(RESET_CREDITS_URL)) { console.log( PREFIX, 'Reset credits detected' ); extractResetCredits( obj, url ); return; } // Avoid recursively scanning every JSON payload ChatGPT receives. if (!CANDIDATE_URL_RE.test(url)) { return; } const raw = JSON.stringify(obj); if (RELATED_KEY_RE.test(raw)) { scanOther( obj, url ); } } // --------------------------------------------------------------------- // Hook fetch // --------------------------------------------------------------------- const originalFetch = window.fetch; if (originalFetch) { window.fetch = async function (...args) { const response = await originalFetch.apply( this, args ); try { let url = ''; if ( typeof args[0] === 'string' ) { url = args[0]; } else if (args[0]?.url) { url = args[0].url; } const cloned = response.clone(); cloned .text() .then( text => { inspectResponse( text, url ); } ) .catch(() => {}); } catch (_) {} return response; }; } // --------------------------------------------------------------------- // Hook XMLHttpRequest // --------------------------------------------------------------------- const XHR = window.XMLHttpRequest; if (XHR) { const originalOpen = XHR.prototype.open; const originalSend = XHR.prototype.send; XHR.prototype.open = function ( method, url, ...rest ) { this.__grf_url = String(url); return originalOpen.call( this, method, url, ...rest ); }; XHR.prototype.send = function (...args) { this.addEventListener( 'load', function () { try { if ( typeof this.responseText === 'string' ) { inspectResponse( this.responseText, this.__grf_url || '' ); } } catch (_) {} } ); return originalSend.apply( this, args ); }; } // --------------------------------------------------------------------- // Start // --------------------------------------------------------------------- ensurePanel(); console.log( PREFIX, 'v1.16 started', { timeZone: LOCAL_TIME_ZONE, language: UI_LANG, initialState: 'collapsed' } ); })();