Greasy Fork

Greasy Fork is available in English.

GLM Coding Plan抢购助手

智能监控,支持配置多级备选抢购;自动穿透限流弹窗;默认使用拼团折扣码更优惠,介意误用

当前为 2026-04-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GLM Coding Plan抢购助手
// @namespace    http://tampermonkey.net/
// @version      6.1
// @description  智能监控,支持配置多级备选抢购;自动穿透限流弹窗;默认使用拼团折扣码更优惠,介意误用
// @author       mumumi
// @include      https://*bigmodel.cn/glm-coding*
// @include      https://*bigmodel.cn/html/rate-limit.html*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bigmodel.cn
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @license      GNU GPLv3
// ==/UserScript==

(function () {
    'use strict';

    // ======================== 数据字典 ========================
    const TABS_MAP = { 1: '连续包月', 2: '连续包季', 3: '连续包年' };
    const PKGS_MAP = { 1: 'Lite', 2: 'Pro', 3: 'Max' };
    const STORAGE_KEY = 'glm_coding_config_v5';

    // ======================== 配置 ========================
    const DEFAULT_CONFIG = {
        TABS_PRIORITY: '1',
        PACKAGES_PRIORITY: '2,3,1',
        CHECK_INTERVAL: 100,
        SMART_REFRESH: true,
    };
    function loadConfig() {
        try {
            const s = GM_getValue(STORAGE_KEY, null);
            return s ? { ...DEFAULT_CONFIG, ...JSON.parse(s) } : { ...DEFAULT_CONFIG };
        } catch { return { ...DEFAULT_CONFIG }; }
    }
    function saveConfig(cfg) { GM_setValue(STORAGE_KEY, JSON.stringify(cfg)); }
    const CONFIG = loadConfig();

    // ======================== 限流页跳回 ========================
    if (location.href.includes('rate-limit.html')) {
        location.replace(RETURN_URL);
        return;
    }

    // ======================== 扫描队列构建 ========================
    // 按用户配置:先遍历每个 tab,在 tab 内按套餐优先级扫描
    const tabs = String(CONFIG.TABS_PRIORITY).split(',').map(Number).filter(Boolean);
    const pkgs = String(CONFIG.PACKAGES_PRIORITY).split(',').map(Number).filter(Boolean);
    const scanQueue = tabs.flatMap(t => pkgs.map(p => ({ tab: t, pkg: p })));

    // ======================== 状态机变量 ========================
    // 主状态: SCANNING | TASK_UNIT | SLEEPING | DONE
    let state = 'SCANNING';

    // SCANNING
    let qIdx = 0;
    let sweepRestocks = [];   // 本轮扫到的补货时间
    let lastTabSwitch = 0;   // 上次切 tab 的时间戳,用于等 Vue 重绘

    // TASK_UNIT
    let taskTarget = null;    // { tab, pkg }
    let taskPhase = 'IDLE';  // IDLE | WAITING
    let taskClickTime = 0;
    let taskRLCount = 0;     // 本任务单元内连续限流次数
    const MAX_RL = 3;        // 连续限流几次后刷新页面
    const MODAL_TIMEOUT = 5000; // 点击后等待弹窗的超时时间(ms)

    // SLEEPING
    let sleepUntil = 0;

    // ======================== 工具函数 ========================
    function parseRestock(text) {
        const m = (text || '').match(/0?(\d{1,2})月0?(\d{1,2})日\s*(\d{1,2}):0?(\d{1,2})/);
        if (!m) return null;
        const now = new Date();
        const t = new Date(now.getFullYear(), +m[1] - 1, +m[2], +m[3], +m[4]);
        const dateStr = `${+m[1]}月${+m[2]}日`;
        return { dateStr, msUntil: t - now };
    }
    function todayStr() {
        const d = new Date();
        return `${d.getMonth() + 1}月${d.getDate()}日`;
    }
    function fmt(ms) {
        if (ms <= 0) return '0秒';
        const s = Math.floor(ms / 1000), m = Math.floor(s / 60), h = Math.floor(m / 60);
        if (h > 0) return `${h}h${m % 60}m`;
        if (m > 0) return `${m}分${s % 60}秒`;
        return `${s}秒`;
    }

    /**
     * 梯度休眠时长计算(距补货时间 → 本次页面刷新间隔)
     *
     *  > 60min  → 4min   (防浏览器休眠选 4min 而非更长)
     *  > 30min  → 3min
     *  > 15min  → 2min
     *  >  5min  → 1min
     *  >  2min  → 30s
     *  >  1min  → 10s
     *  >  10s   → 3s    (极短刷新,精确卡点)
     *  ≤  10s   → 0     (不刷新,原地高频轮询)
     */
    function calcSleepMs(msUntil) {
        if (msUntil > 3600000) return 240000;
        if (msUntil > 1800000) return 180000;
        if (msUntil >  900000) return 120000;
        if (msUntil >  300000) return  60000;
        if (msUntil >  120000) return  30000;
        if (msUntil >   60000) return  10000;
        if (msUntil >   10000) return   3000;
        return 0;
    }

    // ======================== DOM 访问 ========================
    const tabEl = (n) =>
        document.querySelector(`#switchTabBox > div > .switch-tab-item:nth-child(${n + 1})`);

    const btnEl = (n) =>
        document.querySelector(`.glm-coding-package-list > div:nth-child(${n}) > div > .package-card-btn-box > button`);

    // 按钮是否可以立即购买
    const canBuy = (b) =>
        b && !b.disabled && !b.classList.contains('is-disabled') && (b.innerText || '').includes('特惠订阅');

    // 按钮是否明确显示售罄/补货
    const isSoldOut = (b) =>
        /售罄|补货|暂时/.test((b?.innerText) || '');

    // ======================== 弹窗检测 ========================
    function findRLModal() {
        for (const w of document.querySelectorAll('.el-dialog__wrapper')) {
            if (getComputedStyle(w).display !== 'none' &&
                (w.innerText || '').includes('当前购买人数较多')) {
                return w;
            }
        }
        return null;
    }
    function isSuccessDialog() {
        const d = document.querySelector('.pay-success-dialog-box');
        if (!d) return false;
        const w = d.closest('.el-dialog__wrapper');
        return w ? getComputedStyle(w).display !== 'none' : false;
    }
    function closeModal(w) { w?.querySelector('.el-dialog__close')?.click(); }

    // ======================== 底部状态栏 ========================
    let _bar = null;
    function setBar(html, bg = '#1677ff') {
        if (!_bar) {
            _bar = document.createElement('div');
            _bar.id = 'glm-status-bar';
            _bar.style.cssText = `
                position:fixed;bottom:0;left:0;right:0;z-index:2147483647;
                padding:7px 16px;font:13px/1.5 system-ui,sans-serif;color:#fff;
                display:flex;align-items:center;justify-content:space-between;
                box-shadow:0 -2px 8px rgba(0,0,0,.25);transition:background .4s;`;
            const close = document.createElement('button');
            close.textContent = '×';
            close.style.cssText = `background:rgba(255,255,255,.2);border:none;color:#fff;
                width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:16px;
                line-height:1;flex-shrink:0;`;
            close.onclick = () => { _bar.remove(); _bar = null; };
            const msg = document.createElement('span');
            _bar.appendChild(msg);
            _bar.appendChild(close);
            document.body.appendChild(_bar);
        }
        _bar.style.background = bg;
        _bar.firstElementChild.innerHTML = `🤖 <b>抢购助手</b> &nbsp;|&nbsp; ${html}`;
    }

    // ======================== 推广弹窗(多平台版) ========================
    function triggerPromo() {
        if (Date.now() > new Date('2026-04-30T23:59:59').getTime()) {
            setBar('所有套餐今日售罄,脚本停止。', '#434343');
            return;
        }
        const PROVIDERS = [
            { name: 'MiniMax',   desc: '¥29起,赠视频/语音/音乐额度', color: '#4CAF50', url: 'https://platform.minimaxi.com/subscribe/token-plan?code=FoGlERWIF3&source=link' },
            { name: '字节·方舟', desc: '首月低至¥8.9,多模型切换',    color: '#FF6B35', url: 'https://volcengine.com/L/YIeVPueJ2O4/' },
            { name: '讯飞星辰',  desc: '¥19起,首月¥3.9最低门槛',    color: '#00BFFF', url: 'https://maas.xfyun.cn/packageSubscription?inviteCode=MAAS-C6BE3A3B' },
            { name: '无问芯穹',  desc: '多模型聚合,首月¥19.9',       color: '#7B68EE', url: 'https://cloud.infini-ai.com/login?redirect=/genstudio/invitation&invite_code=IyveoKRS' },
        ];
        const rows = PROVIDERS.map(p => `
            <a href="${p.url}" target="_blank" style="display:flex;align-items:center;
               justify-content:space-between;padding:10px 14px;border-radius:8px;
               text-decoration:none;border:1px solid #eee;background:#fafafa;transition:all .2s;"
               onmouseover="this.style.background='#f0f7ff';this.style.borderColor='#91d5ff';"
               onmouseout="this.style.background='#fafafa';this.style.borderColor='#eee';">
                <div style="display:flex;align-items:center;gap:10px;">
                    <span style="width:8px;height:8px;border-radius:50%;background:${p.color};
                        display:inline-block;flex-shrink:0;"></span>
                    <span style="font-weight:600;font-size:14px;color:#111;">${p.name}</span>
                    <span style="font-size:12px;color:#888;">${p.desc}</span>
                </div>
                <span style="font-size:12px;color:${p.color};font-weight:500;flex-shrink:0;">立即开通 →</span>
            </a>`).join('');

        const ov = document.createElement('div');
        ov.style.cssText = `position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:2147483646;
            display:flex;align-items:center;justify-content:center;
            backdrop-filter:blur(5px);font-family:system-ui,sans-serif;`;
        ov.innerHTML = `
            <div style="background:#fff;width:520px;border-radius:16px;overflow:hidden;
                box-shadow:0 25px 50px -12px rgba(0,0,0,.5);max-height:90vh;
                display:flex;flex-direction:column;">
                <div style="background:linear-gradient(135deg,#1e3c72,#2a5298);
                    padding:24px 24px 20px;color:#fff;flex-shrink:0;">
                    <h2 style="margin:0 0 6px;font-size:20px;">GLM Coding Plan 全部售罄 🫠</h2>
                    <p style="margin:0;opacity:.85;font-size:14px;">配置的所有套餐今日已售罄,补货后脚本将继续监控</p>
                </div>
                <div style="padding:16px 20px;overflow-y:auto;flex:1;">
                    <div style="font-size:13px;color:#888;margin-bottom:12px;">
                        👇 以下平台也有编程套餐和折扣链接,点击直达</div>
                    <div style="display:flex;flex-direction:column;gap:8px;">${rows}</div>
                </div>
                <div style="padding:14px 20px;border-top:1px solid #f0f0f0;flex-shrink:0;text-align:right;">
                    <button id="promo-x" style="background:none;border:1px solid #ddd;color:#888;
                        padding:7px 18px;border-radius:6px;cursor:pointer;font-size:13px;">
                        关闭并停止脚本</button>
                </div>
            </div>`;
        document.body.appendChild(ov);
        ov.querySelector('#promo-x').onclick = () => ov.remove();
        ov.onclick = (e) => { if (e.target === ov) ov.remove(); };
    }

    // ======================== 配置面板 ========================
    function buildTransferBox(container, dataMap, selectedStr, title) {
        const sel = selectedStr.split(',').filter(Boolean);
        const avail = Object.keys(dataMap).filter(k => !sel.includes(k));
        container.innerHTML = `
            <div style="font-size:13px;font-weight:bold;margin-bottom:8px;color:#444;">${title}</div>
            <div style="display:flex;align-items:stretch;gap:10px;margin-bottom:20px;height:140px;">
                <div style="flex:1;border:1px solid #ddd;border-radius:6px;display:flex;
                    flex-direction:column;background:#fafafa;">
                    <div style="padding:6px 10px;border-bottom:1px solid #ddd;font-size:12px;
                        color:#666;background:#f0f0f0;border-radius:6px 6px 0 0;">备选池</div>
                    <ul class="tf-left" style="list-style:none;padding:5px;margin:0;flex:1;overflow-y:auto;">
                        ${avail.map(k => `<li data-val="${k}" class="tf-item">${dataMap[k]}</li>`).join('')}
                    </ul>
                </div>
                <div style="display:flex;flex-direction:column;justify-content:center;gap:8px;">
                    <button type="button" class="tf-btn tf-r">▶</button>
                    <button type="button" class="tf-btn tf-l">◀</button>
                </div>
                <div style="flex:1;border:1px solid #ddd;border-radius:6px;display:flex;
                    flex-direction:column;background:#fff;">
                    <div style="padding:6px 10px;border-bottom:1px solid #ddd;font-size:12px;
                        color:#666;background:#e6f7ff;border-radius:6px 6px 0 0;">选中且排序(自上而下)</div>
                    <ul class="tf-right" style="list-style:none;padding:5px;margin:0;flex:1;overflow-y:auto;">
                        ${sel.map(k => `<li data-val="${k}" class="tf-item">${dataMap[k]}</li>`).join('')}
                    </ul>
                </div>
                <div style="display:flex;flex-direction:column;justify-content:center;gap:8px;">
                    <button type="button" class="tf-btn tf-up">▲</button>
                    <button type="button" class="tf-btn tf-dn">▼</button>
                </div>
            </div>`;
        const L = container.querySelector('.tf-left');
        const R = container.querySelector('.tf-right');
        container.querySelectorAll('ul').forEach(ul => ul.addEventListener('click', e => {
            if (e.target.tagName === 'LI') {
                container.querySelectorAll('.tf-item').forEach(i => i.classList.remove('active'));
                e.target.classList.add('active');
            }
        }));
        container.querySelector('.tf-r').onclick = () => {
            const a = L.querySelector('.active');
            if (a) { R.appendChild(a); a.classList.remove('active'); }
        };
        container.querySelector('.tf-l').onclick = () => {
            const a = R.querySelector('.active');
            if (a) { L.appendChild(a); a.classList.remove('active'); }
        };
        container.querySelector('.tf-up').onclick = () => {
            const a = R.querySelector('.active');
            if (a?.previousElementSibling) R.insertBefore(a, a.previousElementSibling);
        };
        container.querySelector('.tf-dn').onclick = () => {
            const a = R.querySelector('.active');
            if (a?.nextElementSibling) R.insertBefore(a.nextElementSibling, a);
        };
        return () => [...R.querySelectorAll('.tf-item')].map(i => i.dataset.val).join(',');
    }

    function openConfigPanel() {
        document.getElementById('glm-cfg-overlay')?.remove();
        if (!document.getElementById('glm-tf-style')) {
            const s = document.createElement('style');
            s.id = 'glm-tf-style';
            s.textContent = `
                .tf-item{padding:6px 10px;margin-bottom:4px;border-radius:4px;cursor:pointer;
                    font-size:13px;color:#333;border:1px solid transparent;transition:all .15s}
                .tf-item:hover{background:#f5f5f5}
                .tf-item.active{background:#e6f7ff;border-color:#91d5ff;color:#1890ff;font-weight:700}
                .tf-btn{padding:4px 8px;font-size:10px;cursor:pointer;border:1px solid #d9d9d9;
                    border-radius:4px;background:#fff;color:#555;height:28px;transition:.2s}
                .tf-btn:hover{border-color:#40a9ff;color:#40a9ff}`;
            document.head.appendChild(s);
        }
        const ov = document.createElement('div');
        ov.id = 'glm-cfg-overlay';
        ov.style.cssText = `position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:2147483646;
            display:flex;align-items:center;justify-content:center;
            backdrop-filter:blur(2px);font-family:system-ui,sans-serif;`;
        const panel = document.createElement('div');
        panel.style.cssText = `background:#fff;color:#333;width:540px;padding:24px;
            border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.3);`;
        panel.innerHTML = `
            <h3 style="margin:0 0 20px;font-size:18px;color:#1a1a1a;">⚙️ 抢购助手配置</h3>
            <div id="glm-wrap-pkg"></div>
            <div id="glm-wrap-tab"></div>
            <div style="margin-bottom:20px;padding-top:10px;border-top:1px dashed #eee;">
                <label style="display:flex;align-items:center;cursor:pointer;">
                    <input type="checkbox" id="glm-smart" ${CONFIG.SMART_REFRESH ? 'checked' : ''}
                        style="margin-right:8px;">
                    <span style="font-size:14px;color:#555;">
                        启用智能刷新(梯度嗅探补货时间,防止黑IP)</span>
                </label>
            </div>
            <div style="display:flex;justify-content:flex-end;gap:10px;">
                <button id="glm-cancel" style="padding:8px 16px;border:1px solid #ddd;
                    background:#f5f5f5;border-radius:6px;cursor:pointer;color:#666;">取消</button>
                <button id="glm-save" style="padding:8px 20px;border:none;background:#1890ff;
                    color:#fff;border-radius:6px;cursor:pointer;font-weight:700;
                    box-shadow:0 2px 5px rgba(24,144,255,.3);">保存并刷新</button>
            </div>`;
        ov.appendChild(panel);
        document.body.appendChild(ov);

        const getPkgs = buildTransferBox(document.getElementById('glm-wrap-pkg'), PKGS_MAP, CONFIG.PACKAGES_PRIORITY, '套餐优先级');
        const getTabs = buildTransferBox(document.getElementById('glm-wrap-tab'), TABS_MAP, CONFIG.TABS_PRIORITY, '订阅周期优先级');

        panel.querySelector('#glm-cancel').onclick = () => ov.remove();
        panel.querySelector('#glm-save').onclick = () => {
            const p = getPkgs(), t = getTabs();
            if (!p || !t) { alert('请至少各选一个套餐和一个周期!'); return; }
            saveConfig({
                TABS_PRIORITY: t,
                PACKAGES_PRIORITY: p,
                SMART_REFRESH: panel.querySelector('#glm-smart').checked,
                CHECK_INTERVAL: CONFIG.CHECK_INTERVAL,
            });
            ov.remove();
            alert('配置已更新!脚本将基于新优先级重新扫描。');
            location.reload();
        };
        ov.onclick = (e) => { if (e.target === ov) ov.remove(); };
    }
    GM_registerMenuCommand('⚙️ 打开配置面板(高级排序)', openConfigPanel);

    // ======================== 主循环 ========================
    function tick() {
        if (state === 'DONE') return;

        if (state === 'SLEEPING') {
            const rem = sleepUntil - Date.now();
            if (rem <= 0) {
                location.replace('https://www.bigmodel.cn/glm-coding?ic=UMYTQHW8I2&closedialog=true');
            } else {
                setBar(`💤 休眠中,<b>${fmt(rem)}</b> 后刷新页面`, '#434343');
            }
            return;
        }

        if (state === 'TASK_UNIT') { doTaskUnit(); return; }
        if (state === 'SCANNING')  { doScan(); }
    }

    // ──────────────────── SCANNING ────────────────────
    function doScan() {
        if (qIdx >= scanQueue.length) {
            onSweepDone();
            return;
        }
        const item = scanQueue[qIdx];

        // 确保目标 tab 激活
        const te = tabEl(item.tab);
        if (!te) return; // DOM 未就绪
        if (!te.classList.contains('active')) {
            te.click();
            lastTabSwitch = Date.now();
            te.scrollIntoView({ behavior: 'auto', block: 'center' });
            setBar(`🔄 切换到 ${TABS_MAP[item.tab]}...`);
            return;
        }
        // 等 Vue 重绘(切 tab 后至少 400ms)
        if (Date.now() - lastTabSwitch < 400) return;

        const b = btnEl(item.pkg);
        if (canBuy(b)) {
            // 找到可购买按钮 → 进入任务单元
            taskTarget  = { ...item };
            taskPhase   = 'IDLE';
            taskRLCount = 0;
            state = 'TASK_UNIT';
            setBar(`🎯 发现可购!${TABS_MAP[item.tab]} · ${PKGS_MAP[item.pkg]},即将点击...`, '#389e0d');
            return;
        }

        // 收集今日补货时间
        const ri = b && parseRestock(b.innerText);
        if (ri && ri.dateStr === todayStr() && ri.msUntil > 0) {
            sweepRestocks.push(ri);
        }

        setBar(`🔍 扫描 ${TABS_MAP[item.tab]} · ${PKGS_MAP[item.pkg]} (${qIdx + 1}/${scanQueue.length})`);
        qIdx++;
    }

    function onSweepDone() {
        if (!sweepRestocks.length) {
            // 今天全部套餐无货且无补货预告 → 显示推广
            state = 'DONE';
            setBar('📭 今日全部售罄,脚本停止。', '#434343');
            triggerPromo();
            return;
        }

        sweepRestocks.sort((a, b) => a.msUntil - b.msUntil);
        const nearest = sweepRestocks[0];
        const sleep   = calcSleepMs(nearest.msUntil);

        if (sleep === 0) {
            // ≤10s,不刷新,原地高频轮询
            setBar(`⚡ 补货倒计时 <b>${fmt(nearest.msUntil)}</b>,高频监控中!`, '#d4380d');
            qIdx = 0;
            sweepRestocks = [];
            return;
        }

        if (CONFIG.SMART_REFRESH) {
            state      = 'SLEEPING';
            sleepUntil = Date.now() + sleep;
            setBar(
                `💤 补货还需 <b>${fmt(nearest.msUntil)}</b>,` +
                `<b>${fmt(sleep)}</b> 后刷新页面`, '#434343'
            );
        } else {
            // 关闭智能刷新时持续轮询
            qIdx = 0;
            sweepRestocks = [];
        }
    }

    // ──────────────────── TASK_UNIT ────────────────────
    function doTaskUnit() {
        const { tab, pkg } = taskTarget;

        // 确保正确 tab 处于激活状态
        const te = tabEl(tab);
        if (!te) return;
        if (!te.classList.contains('active')) { te.click(); return; }

        const b = btnEl(pkg);

        // ── IDLE:尝试点击 ──
        if (taskPhase === 'IDLE') {
            if (!canBuy(b)) {
                // 按钮不可用(防抖结束后变售罄,或被其他人抢走)
                exitTask();
                return;
            }
            b.click();
            taskClickTime = Date.now();
            taskPhase = 'WAITING';
            setBar(
                `⏳ 已点击 ${TABS_MAP[tab]} · ${PKGS_MAP[pkg]},等待弹窗...` +
                `(限流 ${taskRLCount}/${MAX_RL})`, '#d46b08'
            );
            return;
        }

        // ── WAITING:等待弹窗 ──
        if (taskPhase === 'WAITING') {
            // 1. 检测限流弹窗
            const rlw = findRLModal();
            if (rlw) {
                closeModal(rlw);
                taskRLCount++;
                if (taskRLCount >= MAX_RL) {
                    setBar(`🔁 连续 ${MAX_RL} 次限流,即将刷新页面...`, '#cf1322');
                    setTimeout(() => location.replace('https://www.bigmodel.cn/glm-coding?ic=UMYTQHW8I2&closedialog=true'), 50);
                    return;
                }
                setBar(
                    `⚠️ 限流 ${taskRLCount}/${MAX_RL},弹窗已关闭,稍后重试...`,
                    '#d46b08'
                );
                taskPhase = 'IDLE'; // 回到 IDLE 重新点击
                return;
            }

            // 2. 检测购买成功弹窗
            if (isSuccessDialog()) {
                state = 'DONE';
                setBar('🎉 订阅成功!恭喜!', '#237804');
                return;
            }

            // 3. 超时处理
            if (Date.now() - taskClickTime > MODAL_TIMEOUT) {
                if (isSoldOut(b)) {
                    // 按钮已变售罄(被人抢走了)
                    exitTask();
                } else {
                    // 按钮仍在(可能是防抖期结束,按钮恢复可点),重试
                    taskPhase = 'IDLE';
                }
            }
        }
    }

    /**
     * 退出任务单元:当前套餐售罄,向后推进 qIdx,继续扫描
     * (同 tab 内的其他套餐自然排在队列后面,会继续检查)
     */
    function exitTask() {
        setBar(
            `📦 ${TABS_MAP[taskTarget.tab]} · ${PKGS_MAP[taskTarget.pkg]} 售罄,继续扫下一个...`
        );
        qIdx++;          // 跳过当前已售罄的项
        taskTarget  = null;
        taskPhase   = 'IDLE';
        taskRLCount = 0;
        state = 'SCANNING';
    }

    // ======================== 启动 ========================
    setInterval(tick, CONFIG.CHECK_INTERVAL);

})();