GPT Payment Tool

自动填写表单,完成GPT team/plus订阅

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         GPT Payment Tool
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  自动填写表单,完成GPT team/plus订阅
// @author       Community
// @match        https://chatgpt.com/*
// @match        https://pay.openai.com/*
// @match        https://js.stripe.com/*
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(() => {
    'use strict';

    if (window.__gpt_toolkit__) return;
    window.__gpt_toolkit__ = true;

    GM_addStyle(`
        .gpt-orb{position:fixed;top:100px;right:30px;z-index:999999;cursor:move;user-select:none}
        .gpt-orb__sphere{width:48px;height:48px;background:#000;border-radius:50%;box-shadow:0 2px 8px rgba(0,0,0,.15);display:flex;align-items:center;justify-content:center;font-size:20px;cursor:pointer;transition:box-shadow .2s}
        .gpt-orb__sphere:hover{box-shadow:0 3px 12px rgba(0,0,0,.25)}
        .gpt-orb__panel{position:absolute;top:0;right:60px;background:#fff;border:1px solid #e0e0e0;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.08);padding:12px;width:180px;opacity:0;visibility:hidden;transform:translateX(8px);transition:all .2s}
        .gpt-orb__panel--visible{opacity:1;visibility:visible;transform:translateX(0)}
        .gpt-orb__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid #f0f0f0}
        .gpt-orb__title{font:600 13px -apple-system,system-ui,sans-serif;color:#000}
        .gpt-orb__actions{display:flex;gap:8px;align-items:center}
        .gpt-orb__icon{cursor:pointer;opacity:.5;font-size:14px;transition:opacity .2s;padding:2px;color:#000}
        .gpt-orb__icon:hover{opacity:1}
        .gpt-orb__btn{width:100%;padding:8px;margin:4px 0;border:1px solid #d0d0d0;border-radius:6px;font:600 12px -apple-system,system-ui,sans-serif;cursor:pointer;transition:all .15s;color:#000;background:#fff}
        .gpt-orb__btn:hover{background:#f5f5f5;border-color:#a0a0a0}
        .gpt-orb__btn:active{background:#e8e8e8}
        .gpt-orb__btn:disabled{opacity:.4;cursor:not-allowed;background:#fff}
        .gpt-orb__msg{margin-top:8px;padding:8px;border-radius:4px;font-size:11px;text-align:center;border:1px solid}
        .gpt-orb__msg--info{background:#fafafa;color:#666;border-color:#e0e0e0}
        .gpt-orb__msg--ok{background:#f5f5f5;color:#333;border-color:#d0d0d0}
        .gpt-orb__msg--err{background:#fff;color:#000;border-color:#999}
    `);

    const DB = {
        read: k => GM_getValue(k, null),
        write: (k, v) => GM_setValue(k, v)
    };

    const PRESET = {
        cardNum: '5236860170307039',
        expiry: '01/32',
        cvv: '724',
        holder: 'Danny Johey',
        nation: 'US',
        street: '540 North Water Avenue LOT 43',
        town: 'Gallatin',
        region: 'TN',
        postal: '37066'
    };

    const SCHEMA = [
        ['卡号', 'cardNum'],
        ['有效期 (MM/YY)', 'expiry'],
        ['CVV', 'cvv'],
        ['持卡人姓名', 'holder'],
        ['国家代码', 'nation'],
        ['街道地址', 'street'],
        ['城市', 'town'],
        ['州/省', 'region'],
        ['邮编', 'postal']
    ];

    const loc = location;
    const pageType = {
        stripeCard: loc.href.includes('elements-inner-payment'),
        stripeAddr: loc.href.includes('elements-inner-address'),
        payPortal: loc.hostname === 'pay.openai.com',
        checkoutFlow: loc.hostname === 'chatgpt.com' && loc.href.includes('/checkout/'),
        mainChat: loc.hostname === 'chatgpt.com' && !loc.href.includes('/checkout/')
    };

    const q = sel => document.querySelector(sel);
    const qMulti = selectors => {
        for (const s of selectors) {
            const el = q(s);
            if (el) return el;
        }
        return null;
    };

    const poll = (condition, interval = 50, maxAttempts = 60) => {
        return new Promise(resolve => {
            let attempts = 0;
            const timer = setInterval(() => {
                if (condition() || ++attempts >= maxAttempts) {
                    clearInterval(timer);
                    resolve();
                }
            }, interval);
        });
    };

    const fillInput = (element, value) => {
        if (!element || !value) return;
        element.focus();
        const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
        if (desc && desc.set) desc.set.call(element, value);
        else element.value = value;
        ['input', 'change', 'blur'].forEach(evt => {
            element.dispatchEvent(new Event(evt, { bubbles: true }));
        });
    };

    const fillSelect = (element, value) => {
        if (!element) return;
        element.focus();
        const desc = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value');
        if (desc && desc.set) desc.set.call(element, value);
        else element.value = value;
        element.dispatchEvent(new Event('change', { bubbles: true }));
    };

    const toggleCheck = element => {
        if (element && !element.checked) {
            element.click();
            element.checked = true;
        }
    };

    const clip = text => GM_setClipboard(text, 'text');

    const fetchAuth = async () => {
        const resp = await fetch('/api/auth/session');
        if (resp.status === 403) throw new Error('访问被拒绝');
        const json = await resp.json();
        if (!json.accessToken) throw new Error('未登录');
        return json.accessToken;
    };

    const fetchUserEmail = async () => {
        try {
            const resp = await fetch('/api/auth/session');
            if (!resp.ok) return null;
            const json = await resp.json();
            return json?.user?.email || null;
        } catch {
            return null;
        }
    };

    const createPlusCheckout = async () => {
        const token = await fetchAuth();
        const resp = await fetch('https://chatgpt.com/backend-api/payments/checkout', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                plan_type: 'plus',
                checkout_ui_mode: 'hosted',
                cancel_url: 'https://chatgpt.com/',
                success_url: 'https://chatgpt.com/'
            })
        });
        const json = await resp.json();
        if (json.url) return json.url;
        throw new Error(json.detail || '请求失败');
    };

    const createTeamCheckout = async () => {
        const token = await fetchAuth();
        const resp = await fetch('https://chatgpt.com/backend-api/payments/checkout', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                plan_name: 'chatgptteamplan',
                team_plan_data: {
                    workspace_name: 'MyTeam',
                    price_interval: 'month',
                    seat_quantity: 5
                },
                promo_campaign: {
                    promo_campaign_id: 'team-1-month-free',
                    is_coupon_from_query_param: true
                },
                checkout_ui_mode: 'custom'
            })
        });
        const json = await resp.json();
        if (json.checkout_session_id) {
            return `https://chatgpt.com/checkout/openai_llc/${json.checkout_session_id}`;
        }
        throw new Error(json.detail || '请求失败');
    };

    const populateStripeCard = cfg => {
        fillInput(qMulti(['#payment-numberInput', '[name="number"]']), cfg.cardNum);
        fillInput(qMulti(['#payment-expiryInput', '[name="expiry"]']), cfg.expiry);
        fillInput(qMulti(['#payment-cvcInput', '[name="cvc"]']), cfg.cvv);
    };

    const populateStripeAddress = cfg => {
        fillInput(qMulti(['#billingAddress-nameInput', '[name="name"]']), cfg.holder);
        fillSelect(qMulti(['#billingAddress-countryInput', '[name="country"]']), cfg.nation);
        fillInput(qMulti(['#billingAddress-addressLine1Input', '[name="addressLine1"]']), cfg.street);
        fillInput(qMulti(['#billingAddress-localityInput', '[name="locality"]']), cfg.town);
        fillSelect(qMulti(['#billingAddress-administrativeAreaInput', '[name="administrativeArea"]']), cfg.region);
        fillInput(qMulti(['#billingAddress-postalCodeInput', '[name="postalCode"]']), cfg.postal);
    };

    const populatePaymentPortal = cfg => {
        fillInput(q('#cardNumber'), cfg.cardNum);
        fillInput(q('#cardExpiry'), cfg.expiry);
        fillInput(q('#cardCvc'), cfg.cvv);
        fillInput(q('#billingName'), cfg.holder);
        fillSelect(q('#billingCountry'), cfg.nation);
        fillInput(q('#billingAddressLine1'), cfg.street);
        fillInput(q('#billingLocality'), cfg.town);
        fillInput(q('#billingPostalCode'), cfg.postal);
        fillSelect(q('#billingAdministrativeArea'), cfg.region);
        toggleCheck(q('#termsOfServiceConsentCheckbox'));
    };

    const configureSettings = (isFirstTime = false) => {
        const existing = { ...PRESET, ...(DB.read('settings') || {}) };
        if (isFirstTime && DB.read('settings')) return existing;

        if (isFirstTime) {
            alert('⚙️ 首次使用,请配置支付信息');
        }

        const updated = {};
        for (const [label, key] of SCHEMA) {
            const input = prompt(`${label}:`, existing[key] || '');
            if (input === null) return null;
            updated[key] = input;
        }

        DB.write('settings', updated);
        if (!isFirstTime) alert('✅ 设置已保存');

        return { ...PRESET, ...updated };
    };

    let orbEl = null;
    let panelEl = null;
    let msgEl = null;
    let isPanelVisible = false;

    const notify = (text, type = 'info') => {
        if (!msgEl) return;
        msgEl.textContent = text;
        msgEl.className = `gpt-orb__msg gpt-orb__msg--${type}`;
        msgEl.style.display = 'block';
    };

    const togglePanel = () => {
        isPanelVisible = !isPanelVisible;
        if (panelEl) {
            panelEl.classList.toggle('gpt-orb__panel--visible', isPanelVisible);
        }
    };

    const makeDraggable = (element) => {
        let isDragging = false;
        let currentX;
        let currentY;
        let initialX;
        let initialY;
        let xOffset = 0;
        let yOffset = 0;

        const sphere = element.querySelector('.gpt-orb__sphere');

        sphere.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        sphere.addEventListener('touchstart', dragStart);
        document.addEventListener('touchmove', drag);
        document.addEventListener('touchend', dragEnd);

        function dragStart(e) {
            if (e.type === 'touchstart') {
                initialX = e.touches[0].clientX - xOffset;
                initialY = e.touches[0].clientY - yOffset;
            } else {
                initialX = e.clientX - xOffset;
                initialY = e.clientY - yOffset;
            }

            if (e.target === sphere) {
                isDragging = true;
            }
        }

        function drag(e) {
            if (isDragging) {
                e.preventDefault();

                if (e.type === 'touchmove') {
                    currentX = e.touches[0].clientX - initialX;
                    currentY = e.touches[0].clientY - initialY;
                } else {
                    currentX = e.clientX - initialX;
                    currentY = e.clientY - initialY;
                }

                xOffset = currentX;
                yOffset = currentY;

                setTranslate(currentX, currentY, element);
            }
        }

        function dragEnd(e) {
            if (isDragging) {
                initialX = currentX;
                initialY = currentY;
                isDragging = false;
            }
        }

        function setTranslate(xPos, yPos, el) {
            el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
        }
    };

    const buildWidget = () => {
        if (q('.gpt-orb')) return;

        orbEl = document.createElement('div');
        orbEl.className = 'gpt-orb';

        let template = `
            <div class="gpt-orb__sphere">💳</div>
            <div class="gpt-orb__panel">
                <div class="gpt-orb__header">
                    <span class="gpt-orb__title">支付工具</span>
                    <div class="gpt-orb__actions">
                        <span class="gpt-orb__icon" data-action="settings">⚙</span>
                        <span class="gpt-orb__icon" data-action="close">✕</span>
                    </div>
                </div>
        `;

        if (pageType.mainChat) {
            template += `
                <button class="gpt-orb__btn" data-action="plus">Plus 订阅</button>
                <button class="gpt-orb__btn" data-action="team">Team 订阅</button>
            `;
        }

        template += `<div class="gpt-orb__msg" style="display:none"></div></div>`;

        orbEl.innerHTML = template;
        document.body.appendChild(orbEl);

        panelEl = orbEl.querySelector('.gpt-orb__panel');
        msgEl = orbEl.querySelector('.gpt-orb__msg');
        const sphere = orbEl.querySelector('.gpt-orb__sphere');

        makeDraggable(orbEl);

        sphere.addEventListener('click', (e) => {
            if (!e.defaultPrevented) {
                togglePanel();
            }
        });

        orbEl.querySelector('[data-action="close"]').onclick = () => {
            togglePanel();
        };

        const settingsIcon = orbEl.querySelector('[data-action="settings"]');
        if (settingsIcon) {
            settingsIcon.onclick = () => configureSettings();
        }

        const plusBtn = orbEl.querySelector('[data-action="plus"]');
        if (plusBtn) {
            plusBtn.onclick = async function() {
                this.textContent = '生成中...';
                this.disabled = true;
                try {
                    const link = await createPlusCheckout();
                    clip(link);
                    notify('✓ Plus 链接已复制', 'ok');
                    console.log('[工具箱] Plus:', link);
                } catch (err) {
                    notify(`✕ ${err.message}`, 'err');
                    console.error('[工具箱] 错误:', err);
                }
                this.textContent = 'Plus 订阅';
                this.disabled = false;
            };
        }

        const teamBtn = orbEl.querySelector('[data-action="team"]');
        if (teamBtn) {
            teamBtn.onclick = async function() {
                this.textContent = '生成中...';
                this.disabled = true;
                try {
                    const link = await createTeamCheckout();
                    clip(link);
                    notify('✓ Team 链接已复制', 'ok');
                    console.log('[工具箱] Team:', link);
                } catch (err) {
                    notify(`✕ ${err.message}`, 'err');
                    console.error('[工具箱] 错误:', err);
                }
                this.textContent = 'Team 订阅';
                this.disabled = false;
            };
        }
    };

    const execute = async () => {
        const timestamp = Date.now();

        if (pageType.stripeCard) {
            await poll(() => qMulti(['#payment-numberInput', '[name="number"]']));
            const settings = configureSettings(true);
            if (settings) {
                populateStripeCard(settings);
                DB.write('completed', timestamp);
                console.log('[工具箱] Stripe 卡片信息已填充');
            }
        } else if (pageType.stripeAddr) {
            await poll(() => qMulti(['#billingAddress-nameInput', '[name="name"]']));
            const settings = configureSettings(true);
            if (settings) {
                populateStripeAddress(settings);
                DB.write('completed', timestamp);
                console.log('[工具箱] Stripe 地址信息已填充');
            }
        } else if (pageType.payPortal) {
            await poll(() => q('#cardNumber') || q('#email'));
            buildWidget();
            notify('正在填充...', 'info');

            const cachedEmail = DB.read('userEmail');
            if (cachedEmail) fillInput(q('#email'), cachedEmail);

            const settings = configureSettings(true);
            if (settings) {
                populatePaymentPortal(settings);
                notify('✓ 填充完成', 'ok');
                console.log('[工具箱] 支付页面表单已填充');
            } else {
                notify('点击 ⚙ 配置', 'info');
            }
        } else if (pageType.checkoutFlow) {
            await poll(() => document.body);

            const email = await fetchUserEmail();
            if (email) {
                DB.write('userEmail', email);
                console.log('[工具箱] 邮箱已缓存:', email);
            }

            await poll(() => q('#email'), 100, 100);
            if (email) fillInput(q('#email'), email);

            buildWidget();
            if (DB.read('settings')) {
                notify('填充中...', 'info');
            } else {
                notify('点击 ⚙ 配置', 'info');
            }

            const monitor = setInterval(() => {
                if (DB.read('completed') > timestamp) {
                    clearInterval(monitor);
                    notify('✓ 填充完成', 'ok');
                }
            }, 200);

            setTimeout(() => clearInterval(monitor), 30000);
        } else if (pageType.mainChat) {
            const email = await fetchUserEmail();
            if (email) {
                DB.write('userEmail', email);
                console.log('[工具箱] 邮箱已缓存:', email);
            }

            setTimeout(buildWidget, 1000);
        }
    };

    execute().catch(err => console.error('[工具箱] 初始化错误:', err));
})();