907 lines · 36.6 KB · Swift
RawDownload
#if os(macOS)let openAIDashboardScrapeScript = """     (() => {      const textOf = el => {        const raw = el && (el.innerText || el.textContent) ? String(el.innerText || el.textContent) : '';        return raw.trim();      };      const parseHexColor = (color) => {        if (!color) return null;        const c = String(color).trim().toLowerCase();        if (c.startsWith('#')) {          if (c.length === 4) {            return '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];          }          if (c.length === 7) return c;          return c;        }        const m = c.match(/^rgba?\\(([^)]+)\\)$/);        if (m) {          const parts = m[1].split(',').map(x => parseFloat(x.trim())).filter(x => Number.isFinite(x));          if (parts.length >= 3) {            const r = Math.max(0, Math.min(255, Math.round(parts[0])));            const g = Math.max(0, Math.min(255, Math.round(parts[1])));            const b = Math.max(0, Math.min(255, Math.round(parts[2])));            const toHex = n => n.toString(16).padStart(2, '0');            return '#' + toHex(r) + toHex(g) + toHex(b);          }        }        return c;      };      const reactPropsOf = (el) => {        if (!el) return null;        try {          const keys = Object.keys(el);          const propsKey = keys.find(k => k.startsWith('__reactProps$'));          if (propsKey) return el[propsKey] || null;          const fiberKey = keys.find(k => k.startsWith('__reactFiber$'));          if (fiberKey) {            const fiber = el[fiberKey];            return (fiber && (fiber.memoizedProps || fiber.pendingProps)) || null;          }        } catch {}        return null;      };      const reactFiberOf = (el) => {        if (!el) return null;        try {          const keys = Object.keys(el);          const fiberKey = keys.find(k => k.startsWith('__reactFiber$'));          return fiberKey ? (el[fiberKey] || null) : null;        } catch {          return null;        }      };      const nestedBarMetaOf = (root) => {        if (!root || typeof root !== 'object') return null;        const queue = [root];        const seen = typeof WeakSet !== 'undefined' ? new WeakSet() : null;        let steps = 0;        while (queue.length && steps < 250) {          const cur = queue.shift();          steps++;          if (!cur || typeof cur !== 'object') continue;          if (seen) {            if (seen.has(cur)) continue;            seen.add(cur);          }          if (cur.payload && (cur.dataKey || cur.name || cur.value !== undefined)) return cur;          const values = Array.isArray(cur) ? cur : Object.values(cur);          for (const v of values) {            if (v && typeof v === 'object') queue.push(v);          }        }        return null;      };      const barMetaFromElement = (el) => {        const direct = reactPropsOf(el);        if (direct && direct.payload && (direct.dataKey || direct.name || direct.value !== undefined)) return direct;         const fiber = reactFiberOf(el);        if (fiber) {          let cur = fiber;          for (let i = 0; i < 10 && cur; i++) {            const props = (cur.memoizedProps || cur.pendingProps) || null;            if (props && props.payload && (props.dataKey || props.name || props.value !== undefined)) return props;            const nested = props ? nestedBarMetaOf(props) : null;            if (nested) return nested;            cur = cur.return || null;          }        }         if (direct) {          const nested = nestedBarMetaOf(direct);          if (nested) return nested;        }        return null;      };      const normalizeHref = (raw) => {        if (!raw) return null;        const href = String(raw).trim();        if (!href) return null;        if (href.startsWith('http://') || href.startsWith('https://')) return href;        if (href.startsWith('//')) return window.location.protocol + href;        if (href.startsWith('/')) return window.location.origin + href;        return window.location.origin + '/' + href;      };      const isLikelyCreditsURL = (raw) => {        if (!raw) return false;        try {          const url = new URL(raw, window.location.origin);          if (!url.host || !url.host.includes('chatgpt.com')) return false;          const path = url.pathname.toLowerCase();          return (            path.includes('settings') ||            path.includes('usage') ||            path.includes('billing') ||            path.includes('credits')          );        } catch {          return false;        }      };      const purchaseTextMatches = (text) => {        const lower = String(text || '').trim().toLowerCase();        if (!lower) return false;        if (lower.includes('add more')) return true;        if (!lower.includes('credit')) return false;        return (          lower.includes('buy') ||          lower.includes('add') ||          lower.includes('purchase') ||          lower.includes('top up') ||          lower.includes('top-up')        );      };      const elementLabel = (el) => {        if (!el) return '';        return (          textOf(el) ||          el.getAttribute('aria-label') ||          el.getAttribute('title') ||          ''        );      };      const urlFromProps = (props) => {        if (!props || typeof props !== 'object') return null;        const candidates = [          props.href,          props.to,          props.url,          props.link,          props.destination,          props.navigateTo        ];        for (const candidate of candidates) {          if (typeof candidate === 'string' && candidate.trim()) {            return normalizeHref(candidate);          }        }        return null;      };      const purchaseURLFromElement = (el) => {        if (!el) return null;        const isAnchor = el.tagName && el.tagName.toLowerCase() === 'a';        const anchor = isAnchor ? el : (el.closest ? el.closest('a') : null);        const anchorHref = anchor ? anchor.getAttribute('href') : null;        const dataHref = el.getAttribute          ? (el.getAttribute('data-href') ||            el.getAttribute('data-url') ||            el.getAttribute('data-link') ||            el.getAttribute('data-destination'))          : null;        const propHref = urlFromProps(reactPropsOf(el)) || urlFromProps(reactPropsOf(anchor));        const normalized = normalizeHref(anchorHref || dataHref || propHref);        return normalized && isLikelyCreditsURL(normalized) ? normalized : null;      };      const cleanPlanName = (raw) => String(raw || '')        .replace(/\\b(claude|codex|account|plan)\\b/gi, ' ')        .replace(/_/g, ' ')        .replace(/-/g, ' ')        .replace(/\\s+/g, ' ')        .trim();      const codexPlanDisplayName = (raw) => {        const trimmed = String(raw || '').trim();        if (!trimmed) return null;        const lower = trimmed.toLowerCase();        const exact = {          pro: 'Pro 20x',          prolite: 'Pro 5x',          'pro_lite': 'Pro 5x',          'pro-lite': 'Pro 5x',          'pro lite': 'Pro 5x'        };        if (exact[lower]) return exact[lower];        const cleaned = cleanPlanName(trimmed);        if (!cleaned) return trimmed;        if (exact[cleaned.toLowerCase()]) return exact[cleaned.toLowerCase()];        return cleaned.split(' ')          .filter(Boolean)          .map(word => {            const wordLower = word.toLowerCase();            if (wordLower === 'cbp' || wordLower === 'k12') return wordLower.toUpperCase();            if (word === word.toUpperCase() && /[a-z]/i.test(word)) return word;            return word.charAt(0).toUpperCase() + word.slice(1);          })          .join(' ') || cleaned;      };      const normalizePlanValue = (value) => {        const trimmed = String(value || '').trim();        if (!trimmed) return null;        const lower = trimmed.toLowerCase();        const allowed = [          'free',          'plus',          'pro',          'team',          'enterprise',          'business',          'edu',          'education',          'gov',          'premium',          'essential'        ];        if (!allowed.some(token => lower.includes(token))) return null;        return codexPlanDisplayName(trimmed) || cleanPlanName(trimmed);      };      const planCandidate = (key, value) => {        const lower = String(key || '').toLowerCase();        if (!lower.includes('plan') && !lower.includes('tier') && !lower.includes('subscription')) return null;        if (typeof value === 'string') return normalizePlanValue(value);        if (value && typeof value === 'object' && !Array.isArray(value)) {          return normalizePlanValue(value.name) ||            normalizePlanValue(value.displayName) ||            normalizePlanValue(value.tier);        }        return null;      };      const findPlan = (root) => {        if (!root || typeof root !== 'object') return null;        const queue = [root];        const seenObjects = typeof WeakSet !== 'undefined' ? new WeakSet() : null;        let index = 0;        let seen = 0;        while (index < queue.length && seen < 6000) {          const cur = queue[index++];          seen++;          if (!cur || typeof cur !== 'object') continue;          if (seenObjects) {            if (seenObjects.has(cur)) continue;            seenObjects.add(cur);          }          if (Array.isArray(cur)) {            for (const v of cur) {              if (v && typeof v === 'object') queue.push(v);            }            continue;          }          for (const [k, v] of Object.entries(cur)) {            const plan = planCandidate(k, v);            if (plan) return plan;            if (v && typeof v === 'object') queue.push(v);          }        }        return null;      };      const parseJSONScript = (id) => {        try {          const node = document.getElementById(id);          const raw = node && node.textContent ? String(node.textContent) : '';          return raw ? JSON.parse(raw) : null;        } catch {          return null;        }      };      const pickLikelyPurchaseButton = (buttons) => {        if (!buttons || buttons.length === 0) return null;        const labeled = buttons.find(btn => {          const label = elementLabel(btn);          if (purchaseTextMatches(label)) return true;          const aria = String(btn.getAttribute('aria-label') || '').toLowerCase();          return aria.includes('credit') || aria.includes('buy') || aria.includes('add');        });        return labeled || buttons[0];      };      const findCreditsPurchaseButton = () => {        const nodes = Array.from(document.querySelectorAll('h1,h2,h3,div,span,p'));        const labelMatch = nodes.find(node => {          const lower = textOf(node).toLowerCase();          return lower === 'credits remaining' || (lower.includes('credits') && lower.includes('remaining'));        });        if (!labelMatch) return null;        let cur = labelMatch;        for (let i = 0; i < 6 && cur; i++) {          const buttons = Array.from(cur.querySelectorAll('button, a'));          const picked = pickLikelyPurchaseButton(buttons);          if (picked) return picked;          cur = cur.parentElement;        }        return null;      };      const dayKeyFromPayload = (payload) => {        if (!payload || typeof payload !== 'object') return null;        const localDayKeyForDate = (date) => {          const year = date.getFullYear();          const month = String(date.getMonth() + 1).padStart(2, '0');          const day = String(date.getDate()).padStart(2, '0');          return `${year}-${month}-${day}`;        };        const keys = ['day', 'date', 'name', 'label', 'x', 'time', 'timestamp'];        for (const k of keys) {          const v = payload[k];          if (typeof v === 'string') {            const s = v.trim();            if (/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return s;            const iso = s.match(/^(\\d{4}-\\d{2}-\\d{2})/);            if (iso) return iso[1];          }          if (typeof v === 'number' && Number.isFinite(v) && (k === 'timestamp' || k === 'time' || k === 'x')) {            try {              const d = new Date(v);              if (!isNaN(d.getTime())) return localDayKeyForDate(d);            } catch {}          }        }        return null;      };      const isSkillUsageServiceKey = (raw) => {        const key = raw === null || raw === undefined ? '' : String(raw).trim().toLowerCase();        return key.startsWith('skillusage:');      };      const displayNameForUsageServiceKey = (raw) => {        const key = raw === null || raw === undefined ? '' : String(raw).trim();        if (!key) return key;        if (isSkillUsageServiceKey(key)) return null;        if (key.toUpperCase() === key && key.length <= 6) return key;        const lower = key.toLowerCase();        if (lower === 'cli') return 'CLI';        if (lower.includes('github') && lower.includes('review')) return 'GitHub Code Review';        const words = lower.replace(/[_-]+/g, ' ').split(' ').filter(Boolean);        return words.map(w => w.length <= 2 ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)).join(' ');      };      const isLikelyCodexUsageService = (raw) => {        const service = raw === null || raw === undefined ? '' : String(raw).trim().toLowerCase();        return (          service === 'cli' ||          service === 'desktop' ||          service === 'desktop app' ||          service === 'vscode' ||          service === 'vs code' ||          service === 'unknown' ||          (service.includes('github') && service.includes('review'))        );      };      const usageChartRootForPath = (path) => {        if (!path || !path.closest) return null;        return (          path.closest('.recharts-wrapper') ||          path.closest('svg.recharts-surface') ||          path.closest('section') ||          path.parentElement ||          null        );      };      const uniqueUsageChartRoots = (paths) => {        const roots = [];        for (const path of paths) {          const root = usageChartRootForPath(path);          if (root && !roots.includes(root)) roots.push(root);        }        return roots;      };      const usageBreakdownTitleScore = (title) => {        const lower = String(title || '').trim().toLowerCase().replace(/\\s+/g, ' ');        if (!lower) return 0;        if (lower === 'usage breakdown') return 1000000;        if (lower.includes('usage breakdown')) return 900000;        if (lower === 'personal usage') return 800000;        if (lower.includes('threads') ||          lower.includes('turns') ||          lower.includes('client') ||          lower.includes('skill') ||          lower.includes('invocation')) return -1000000;        return 0;      };      const titleLikeElements = (scope) => {        try {          return Array.from(scope.querySelectorAll('h1,h2,h3,[role=\"heading\"],div,span,p'))            .filter(el => {              const title = textOf(el);              const lower = title.toLowerCase();              const tag = el.tagName ? el.tagName.toLowerCase() : '';              const isHeading = tag === 'h1' ||                tag === 'h2' ||                tag === 'h3' ||                String(el.getAttribute('role') || '').toLowerCase() === 'heading';              return title.length > 0 &&                title.length <= 80 &&                (                  isHeading ||                  usageBreakdownTitleScore(title) !== 0 ||                  lower.includes('usage breakdown') ||                  lower.includes('threads') ||                  lower.includes('turns') ||                  lower.includes('client') ||                  lower.includes('skill') ||                  lower.includes('invocation')                );            });        } catch {          return [];        }      };      const titleNodePrecedesRoot = (titleNode, root) => {        if (!titleNode || titleNode === root || root.contains(titleNode) || titleNode.contains(root)) return false;        const relation = titleNode.compareDocumentPosition(root);        return Boolean(relation & Node.DOCUMENT_POSITION_FOLLOWING);      };      const nearestScoredChartTitleInScope = (scope, root) => {        let best = null;        for (const titleNode of titleLikeElements(scope)) {          if (!titleNodePrecedesRoot(titleNode, root)) continue;          const title = textOf(titleNode);          const score = usageBreakdownTitleScore(title);          if (score === 0) continue;          if (!best || score >= best.score) best = { title, score };        }        return best ? best.title : '';      };      const chartTitleBoundaryForRoot = (root) => {        if (!root) return null;        try {          return root.closest('section,[role=\"region\"],article') || root.parentElement || null;        } catch {          return root.parentElement || null;        }      };      const nearestTitleTextInScope = (scope, root) => {        if (!scope) return '';        let nearest = null;        for (const titleNode of titleLikeElements(scope)) {          if (titleNodePrecedesRoot(titleNode, root)) nearest = titleNode;        }        return textOf(nearest);      };      const nearestChartTitleTextForRoot = (root) => {        if (!root) return '';        try {          const boundary = chartTitleBoundaryForRoot(root) || root.parentElement || null;          let ancestor = root.parentElement || null;          for (let i = 0; i < 8 && ancestor; i++) {            const scoredTitle = nearestScoredChartTitleInScope(ancestor, root);            if (scoredTitle) return scoredTitle;            if (ancestor === boundary) break;            ancestor = ancestor.parentElement || null;          }           return nearestTitleTextInScope(boundary, root);        } catch {          return '';        }      };      const legendMapForUsageChartRoot = (root) => {        const legendMap = {};        const scopes = [          root,          root && root.parentElement,          root && root.closest ? root.closest('section') : null        ].filter(Boolean);        for (const scope of scopes) {          try {            const legendItems = Array.from(scope.querySelectorAll('div[title]'));            for (const item of legendItems) {              const title = item.getAttribute('title') ? String(item.getAttribute('title')).trim() : '';              const square = item.querySelector('div[style*=\"background-color\"]');              const color = (square && square.style && square.style.backgroundColor)                ? square.style.backgroundColor                : null;              const hex = parseHexColor(color);              if (title && hex) legendMap[hex] = title;            }          } catch {}          if (Object.keys(legendMap).length > 0) break;        }        return legendMap;      };      const parseUsageBreakdownFromChartPaths = (paths, legendMap) => {        const totalsByDay = {}; // day -> service -> value        const addValue = (day, service, value) => {          if (!day || !service || isSkillUsageServiceKey(service)) return false;          if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return false;          if (!totalsByDay[day]) totalsByDay[day] = {};          totalsByDay[day][service] = (totalsByDay[day][service] || 0) + value;          return true;        };        let pointCount = 0;        for (const path of paths) {          const meta = barMetaFromElement(path) || barMetaFromElement(path.parentElement) || null;          if (!meta) continue;

Lines 1–500 of 907.