Bailian Model Expiry Extractor

Accurately extract model name, code, quota (%, 0, or N/M), countdown, expiry, and copy code.

Per 30-07-2025. Zie de nieuwste versie.

// ==UserScript==
// @name         Bailian Model Expiry Extractor
// @name:zh-CN   阿里云百炼模型到期时间提取器
// @namespace    https://greasyforks.org/zh-CN/scripts/543956-bailian-model-expiry-extractor
// @version      1.3.3
// @author       will
// @description  Accurately extract model name, code, quota (%, 0, or N/M), countdown, expiry, and copy code.
// @description:zh-CN 精准提取模型名称、Code、免费额度(支持百分比/无额度)、倒计时、到期时间,一键复制 Code。
// @license      MIT
// @homepage     https://github.com/jwq2011/TamperMonkey-Scripts
// @supportURL   https://github.com/jwq2011/TamperMonkey-Scripts/issues
// @match        https://bailian.console.aliyun.com/console*
// @grant        GM_setClipboard
// @run-at       document-end
// @compatible   tampermonkey
// @compatible   violentmonkey
// ==/UserScript==

(function () {
    'use strict';

    const DEBUG = false;
    const LOG_PREFIX = '[Bailian Expiry+]';

    function log(...args) {
        if (DEBUG) console.log(LOG_PREFIX, ...args);
    }

    let extractedData = [];

    function createFloatingButton() {
        const btnId = 'bailian-extractor-btn';
        if (document.getElementById(btnId)) return;

        const button = document.createElement('button');
        button.id = btnId;
        Object.assign(button.style, {
            position: 'fixed', top: '80px', right: '20px', zIndex: '2147483647',
            backgroundColor: '#ff6a00', color: 'white', border: 'none',
            padding: '12px 16px', borderRadius: '8px', cursor: 'pointer',
            fontSize: '14px', fontWeight: 'bold', boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
            opacity: 0.95, fontFamily: 'Arial, sans-serif'
        });
        button.textContent = '📊 提取模型信息';

        button.addEventListener('click', () => {
            button.disabled = true;
            button.textContent = '🔍 提取中...';

            let needWait = false;

            // 自动切换视图
            if (switchToListView()) {
                needWait = true;
            }

            // 自动展开折叠区域
            if (autoExpandFoldedRows()) {
                needWait = true;
            }

            // 等待 DOM 更新
            setTimeout(() => {
                const data = extractAllModels();
                if (data.length === 0) {
                    alert('❌ 未找到任何模型信息,请确认已打开【模型市场】页面并完全加载。');
                } else {
                    extractedData = data;
                    showResultsModal();
                }
                button.disabled = false;
                button.textContent = '📊 提取模型信息';
            }, needWait ? 1200 : 500);
        });

        document.body.appendChild(button);
        log('✅ 按钮已创建');
    }

    // 自动切换到列表视图(精准判断)
    function switchToListView() {
        // 判断是否已经是列表视图
        const isListView = document.querySelector('.bl-icon-list-line.active__VRFfX');
        if (isListView) {
            log('✅ 当前已是列表视图');
            return false;
        }

        // 否则,点击列表图标切换
        const listViewIcon = document.querySelector('.bl-icon-list-line');
        const button = listViewIcon?.closest('button');

        if (button && button.offsetWidth > 0 && button.offsetHeight > 0) {
            button.click();
            log('✅ 已切换到列表视图');
            return true;
        }

        log('⚠️ 未找到“切换到列表视图”按钮');
        return false;
    }

    // 自动展开折叠区域
    function autoExpandFoldedRows() {
        const expandButtons = [...document.querySelectorAll('button[aria-label="展开"], button[title="展开"]')];
        let clicked = false;
        for (const btn of expandButtons) {
            if (btn.offsetWidth > 0 && btn.offsetHeight > 0) {
                btn.click();
                clicked = true;
                log('✅ 点击展开按钮');
            }
        }
        return clicked;
    }

    // 提取模型信息
    function extractAllModels() {
        log('🔍 开始提取模型数据...');

        const rowSelectors = ['tr[data-row-key]', '.ant-table-row', 'tr[role="row"]'];
        let rows = [];
        for (const sel of rowSelectors) {
            rows = [...document.querySelectorAll(sel)];
            if (rows.length > 0) break;
        }

        if (rows.length === 0) {
            log('❌ 未找到任何行');
            return [];
        }

        const results = [];

        for (const row of rows) {
            // --- 模型名称 ---
            const nameContainer = row.querySelector('.model-name__xEkXf');
            const nameEl = nameContainer?.querySelector('span'); // 只取 span 内容
            const name = (nameEl?.textContent || '未知模型').trim();

            // --- 精准提取 Code ---
            let code = '';
            const spans = row.querySelectorAll('span');
            for (const span of spans) {
                const text = span.textContent.trim();
                if (/^qwen[-\w]*\d/.test(text)) {
                    code = text.toLowerCase();
                    break;
                }
            }
            code = code || '—';

            // --- 免费额度:数值 + 百分比 ---
            let freeQuota = '—';
            let quotaText = '0';
            let percentText = '0%';

            const quotaSpan = row.querySelector('.value__V7Z7e');
            if (quotaSpan) {
                const text = quotaSpan.textContent.trim();
                const match = text.match(/(\d[\d,]*)\s*\/\s*(\d[\d,]+)/);
                if (match) {
                    const used = parseInt(match[1].replace(/,/g, ''));
                    const total = parseInt(match[2].replace(/,/g, ''));
                    quotaText = `${used.toLocaleString()}/${total.toLocaleString()}`;
                }
            }

            const percentSpan = row.querySelector('.efm_ant-progress-text');
            if (percentSpan) {
                const pct = percentSpan.textContent.trim();
                if (/^\d+(\.\d+)?%$/.test(pct)) {
                    percentText = pct;
                }
            }

            if (quotaText !== '0') {
                freeQuota = `${quotaText} · ${percentText}`;
            } else if (/^\d+(\.\d+)?%$/.test(percentText)) {
                freeQuota = percentText;
            } else {
                freeQuota = /无免费额度/.test(row.textContent) ? '0 · 0%' : '—';
            }

            // --- 到期时间 ---
            const expiryMatch = row.textContent.match(/到期时间.?(\d{4}-\d{2}-\d{2})/);
            if (!expiryMatch) continue;

            const expiry = expiryMatch[1];
            const expiryDate = new Date(expiry);
            const today = new Date().setHours(0, 0, 0, 0);
            const daysLeft = Math.ceil((expiryDate - today) / 86400000);
            if (daysLeft < 0) continue;

            results.push({ name, code, freeQuota, daysLeft, expiry });
            log('✅ 提取:', name, code, freeQuota, `剩余 ${daysLeft} 天`, expiry);
        }

        return results.sort((a, b) => a.daysLeft - b.daysLeft);
    }

    // 显示结果弹窗
    function showResultsModal() {
        const modalId = 'bailian-extractor-modal';
        if (document.getElementById(modalId)) document.body.removeChild(document.getElementById(modalId));

        const modal = document.createElement('div');
        modal.id = modalId;
        Object.assign(modal.style, {
            position: 'fixed', top: 0, left: 0, width: '100%', height: '100%',
            backgroundColor: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center',
            justifyContent: 'center', zIndex: '2147483647', fontFamily: 'Arial, sans-serif'
        });

        const content = document.createElement('div');
        Object.assign(content.style, {
            backgroundColor: 'white', width: '95%', maxWidth: '1000px', maxHeight: '85vh',
            overflow: 'auto', borderRadius: '10px', padding: '20px', position: 'relative'
        });

        const title = document.createElement('h3');
        title.textContent = `✅ 提取结果(${extractedData.length} 个模型)`;
        content.appendChild(title);

        if (extractedData.length === 0) {
            content.appendChild(document.createTextNode('未找到有效模型信息。'));
        } else {
            const table = document.createElement('table');
            table.style.width = '100%';
            table.style.borderCollapse = 'collapse';
            table.innerHTML = `
                <thead>
                    <tr style="background:#f5f5f5;">
                        <th style="text-align:left;padding:10px;border:1px solid #ddd;">模型名称</th>
                        <th style="text-align:left;padding:10px;border:1px solid #ddd;">Code</th>
                        <th style="text-align:left;padding:10px;border:1px solid #ddd;">免费额度</th>
                        <th style="text-align:left;padding:10px;border:1px solid #ddd;">倒计时显示</th>
                        <th style="text-align:left;padding:10px;border:1px solid #ddd;">到期时间</th>
                    </tr>
                </thead>
                <tbody></tbody>
            `;

            const tbody = table.querySelector('tbody');
            extractedData.forEach(item => {
                const tr = document.createElement('tr');
                appendCell(tr, item.name);
                appendCodeCell(tr, item.code);
                appendCell(tr, item.freeQuota);
                appendCountdownCell(tr, item.daysLeft);
                appendCell(tr, item.expiry, { color: '#d9534f', fontWeight: 'bold' });
                tbody.appendChild(tr);
            });

            content.appendChild(table);

            const csvBtn = document.createElement('button');
            csvBtn.textContent = '📋 复制为 CSV';
            csvBtn.style.marginTop = '15px';
            csvBtn.style.padding = '10px';
            csvBtn.style.backgroundColor = '#007cba';
            csvBtn.style.color = 'white';
            csvBtn.style.border = 'none';
            csvBtn.style.borderRadius = '4px';
            csvBtn.style.cursor = 'pointer';
            csvBtn.onclick = () => {
                const csv = [
                    ['模型名称', 'Code', '免费额度', '倒计时显示', '到期时间'].join(','),
                    ...extractedData.map(d => [
                        d.name,
                        d.code,
                        d.freeQuota,
                        `剩余 ${d.daysLeft} 天`,
                        d.expiry
                    ].map(s => `"${String(s).replace(/"/g, '""')}"`).join(','))
                ].join('\n');
                navigator.clipboard.writeText(csv).then(() => {
                    csvBtn.textContent = '✅ 已复制!';
                    setTimeout(() => csvBtn.textContent = '📋 复制为 CSV', 2000);
                });
            };
            content.appendChild(csvBtn);
        }

        const close = document.createElement('span');
        close.textContent = '×';
        close.style.position = 'absolute'; close.style.top = '10px'; close.style.right = '16px';
        close.style.fontSize = '24px'; close.style.cursor = 'pointer';
        close.onclick = () => document.body.removeChild(modal);
        content.appendChild(close);

        modal.appendChild(content);
        document.body.appendChild(modal);
    }

    // 工具函数:创建表格单元格
    function appendCell(tr, text, style = {}) {
        const td = document.createElement('td');
        td.style.padding = '10px';
        td.style.border = '1px solid #ddd';
        Object.assign(td.style, style);
        td.textContent = text;
        tr.appendChild(td);
    }

    function appendCodeCell(tr, code) {
        const td = document.createElement('td');
        td.style.padding = '10px';
        td.style.border = '1px solid #ddd';
        td.style.cursor = 'pointer';
        td.style.color = '#007cba';
        td.style.fontWeight = 'bold';
        td.title = '点击复制 Code';
        td.textContent = code;
        td.onclick = () => {
            GM_setClipboard(code);
            td.textContent = '✅ 已复制!';
            setTimeout(() => td.textContent = code, 1500);
        };
        tr.appendChild(td);
    }

    function appendCountdownCell(tr, daysLeft) {
        const td = document.createElement('td');
        td.style.padding = '10px';
        td.style.border = '1px solid #ddd';
        td.style.fontWeight = 'bold';
        td.style.color = daysLeft < 30 ? '#d9534f' :
                        daysLeft < 90 ? '#f0ad4e' : '#5cb85c';
        td.textContent = `剩余 ${daysLeft} 天`;
        tr.appendChild(td);
    }

    // 初始化
    function init() {
        console.log(LOG_PREFIX, '脚本已注入,版本:', GM_info.script.version);
        setTimeout(createFloatingButton, 1000);
    }

    init();
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。