SiliconFlow 模型筛选器

在 SiliconFlow 价格页面添加筛选功能

// ==UserScript==
// @name         SiliconFlow 模型筛选器
// @namespace    siliconflow-model-filter
// @version      2025.0.1
// @description  在 SiliconFlow 价格页面添加筛选功能
// @author       delph1s
// @license      MIT
// @icon         https://siliconflow.cn/logo-footer.svg
// @match        https://siliconflow.cn/pricing*
// @match        https://siliconflow.cn/pricing
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let isExpanded = false;
    let extractedModels = [];

    // 创建样式
    function createStyles() {
        const style = document.createElement('style');
        style.textContent = `
            /* 主容器 */
            #sf-filter-container {
                position: fixed !important;
                bottom: 14px !important;
                right: 14px !important;
                z-index: 99999 !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
            }

            /* 浮动按钮 */
            #sf-filter-container .sf-toggle-btn {
                width: 28px !important;
                height: 28px !important;
                border-radius: 14px !important;
                background: linear-gradient(135deg, rgba(255,255,255,0.25), rgba(255,255,255,0.1)) !important;
                backdrop-filter: blur(10px) !important;
                -webkit-backdrop-filter: blur(10px) !important;
                border: 1px solid rgba(255,255,255,0.2) !important;
                box-shadow: 0 8px 32px rgba(0,0,0,0.1), 0 2px 8px rgba(0,0,0,0.05) !important;
                cursor: pointer !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
                color: #333 !important;
                font-size: 14px !important;
                font-weight: 600 !important;
                margin: 0 !important;
                padding: 0 !important;
            }

            #sf-filter-container .sf-toggle-btn:hover {
                transform: scale(1.2) !important;
                box-shadow: 0 12px 40px rgba(0,0,0,0.15), 0 4px 12px rgba(0,0,0,0.1) !important;
            }

            /* 主面板 */
            #sf-filter-container .sf-panel {
                position: absolute !important;
                bottom: 0 !important;
                right: 0 !important;
                width: 360px !important;
                max-height: 80vh !important;
                background: linear-gradient(135deg, rgba(255,255,255,0.25), rgba(255,255,255,0.1)) !important;
                backdrop-filter: blur(10px) !important;
                -webkit-backdrop-filter: blur(10px) !important;
                border-radius: 14px !important;
                border: 1px solid rgba(255,255,255,0.3) !important;
                box-shadow: 0 20px 60px rgba(0,0,0,0.1), 0 8px 24px rgba(0,0,0,0.05) !important;
                padding: 24px !important;
                transform: scale(0.8) translateX(360px) !important;
                opacity: 0 !important;
                visibility: hidden !important;
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
                overflow: hidden !important;
                margin: 0 !important;
            }

            #sf-filter-container .sf-panel.expanded {
                // animation: sf-panel-bounce-in 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
                transform: scale(1) translateX(0) !important;
                opacity: 1 !important;
                visibility: visible !important;
            }

            @keyframes sf-panel-bounce-in {
                0% {
                    transform: scale(0.3) translateX(300px) !important;
                    opacity: 0 !important;
                    visibility: visible !important;
                }
                25% {
                    transform: scale(0.7) translateX(150px) !important;
                    opacity: 0.7 !important;
                }
                50% {
                    transform: scale(1.1) translateX(-20px) !important;
                    opacity: 0.9 !important;
                }
                65% {
                    transform: scale(0.9) translateX(10px) !important;
                    opacity: 1 !important;
                }
                80% {
                    transform: scale(1.05) translateX(-5px) !important;
                }
                100% {
                    transform: scale(1) translateX(0) !important;
                    opacity: 1 !important;
                    visibility: visible !important;
                }
            }

            /* 标题 */
            #sf-filter-container .sf-title {
                font-size: 24px !important;
                font-weight: 700 !important;
                color: #1d1d1f !important;
                margin: 0 0 14px 0 !important;
                padding: 0 !important;
                text-align: center !important;
                background: linear-gradient(135deg, #007AFF, #5856D6) !important;
                -webkit-background-clip: text !important;
                -webkit-text-fill-color: transparent !important;
                background-clip: text !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
                line-height: 1.2 !important;
                border: none !important;
            }

            /* 关闭按钮 */
            #sf-filter-container .sf-close-btn {
                position: absolute !important;
                top: 14px !important;
                right: 14px !important;
                width: 24px !important;
                height: 24px !important;
                border-radius: 14px !important;
                background: rgba(142, 142, 147, 0) !important;
                color: #8e8e93 !important;
                cursor: pointer !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
                font-size: 14px !important;
                transition: all 0.3s ease !important;
                margin: 0 !important;
                padding: 0 !important;
                border: none !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
            }

            #sf-filter-container .sf-close-btn:hover {
                background: rgba(142, 142, 147, 0.2) !important;
                color: #1d1d1f !important;
            }

            /* 表单组 */
            #sf-filter-container .sf-form-group {
                line-height: 0 !important;
                margin: 0 0 14px 0 !important;
                padding: 0 !important;
            }

            #sf-filter-container .sf-label {
                display: block !important;
                font-size: 16px !important;
                font-weight: 600 !important;
                color: #1d1d1f !important;
                margin: 0 0 7px 0 !important;
                padding: 0 !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
                line-height: 1.2 !important;
                border: none !important;
                background: none !important;
            }

            /* 输入框和选择框 */
            #sf-filter-container .sf-input,
            #sf-filter-container .sf-select {
                width: 100% !important;
                padding: 8px 12px !important;
                background: rgba(142, 142, 147, 0) !important;
                border: 1px solid rgba(0, 122, 255, 0.5) !important;
                border-radius: 7px !important;
                font-size: 14px !important;
                color: #1d1d1f !important;
                transition: all 0.2s ease !important;
                outline: none !important;
                margin: 0 !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
                box-sizing: border-box !important;
            }

            #sf-filter-container .sf-input:focus,
            #sf-filter-container .sf-select:focus {
                background: rgba(255, 255, 255, 0) !important;
                border-color: rgba(0, 122, 255, 1) !important;
                // box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1) !important;
            }

            #sf-filter-container .sf-input::placeholder {
                color: #8e8e93 !important;
            }

            /* 按钮 */
            #sf-filter-container .sf-btn {
                width: 100% !important;
                // height: 36px !important;
                border-radius: 7px !important;
                font-size: 14px !important;
                font-weight: 600 !important;
                cursor: pointer !important;
                transition: all 0.3s ease !important;
                margin: 0 0 14px 0 !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
                outline: none !important;
                padding: 8px 12px !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
                box-sizing: border-box !important;
                border: none !important;
            }

            #sf-filter-container .sf-btn-primary {
                background: linear-gradient(135deg, #007AFFBC, #5856D6BC) !important;
                color: white !important;
            }

            #sf-filter-container .sf-btn-primary:hover {
                transform: translateY(-1px) !important;
                box-shadow: 0 8px 25px rgba(0, 122, 255, 0.3) !important;
            }

            #sf-filter-container .sf-btn-success {
                background: linear-gradient(135deg, #34C759BC, #30D158BC) !important;
                color: white !important;
            }

            #sf-filter-container .sf-btn-success:hover:not(:disabled) {
                transform: translateY(-1px) !important;
                box-shadow: 0 8px 25px rgba(52, 199, 89, 0.3) !important;
            }

            #sf-filter-container .sf-btn:disabled {
                opacity: 0.5 !important;
                cursor: not-allowed !important;
                transform: none !important;
            }

            /* 统计信息 */
            #sf-filter-container .sf-stats {
                text-align: center !important;
                font-size: 14px !important;
                color: #8e8e93 !important;
                margin: 0 0 14px 0 !important;
                padding: 8px 12px !important;
                background: rgba(142, 142, 147, 0.08) !important;
                border-radius: 7px !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
                font-weight: 400 !important;
                border: none !important;
            }

            /* 结果区域 */
            #sf-filter-container .sf-result-area {
                max-height: 200px !important;
                overflow-y: auto !important;
                border-radius: 12px !important;
                background: rgba(142, 142, 147, 0) !important;
                border: 1px solid rgba(142, 142, 147, 0.1) !important;
                margin: 0 !important;
                padding: 0 !important;
            }

            #sf-filter-container .sf-textarea {
                width: 100% !important;
                height: 200px !important;
                padding: 16px !important;
                border: none !important;
                background: transparent !important;
                color: #1d1d1f !important;
                font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace !important;
                font-size: 12px !important;
                line-height: 1.5 !important;
                resize: none !important;
                outline: none !important;
                margin: 0 !important;
                box-sizing: border-box !important;
            }

            #sf-filter-container .sf-textarea::placeholder {
                color: #8e8e93 !important;
            }

            /* 滚动条样式 */
            #sf-filter-container .sf-result-area::-webkit-scrollbar,
            #sf-filter-container .sf-panel::-webkit-scrollbar {
                width: 6px !important;
            }

            #sf-filter-container .sf-result-area::-webkit-scrollbar-track,
            #sf-filter-container .sf-panel::-webkit-scrollbar-track {
                background: rgba(142, 142, 147, 0.1) !important;
                border-radius: 3px !important;
            }

            #sf-filter-container .sf-result-area::-webkit-scrollbar-thumb,
            #sf-filter-container .sf-panel::-webkit-scrollbar-thumb {
                background: rgba(142, 142, 147, 0.3) !important;
                border-radius: 3px !important;
            }

            #sf-filter-container .sf-result-area::-webkit-scrollbar-thumb:hover,
            #sf-filter-container .sf-panel::-webkit-scrollbar-thumb:hover {
                background: rgba(142, 142, 147, 0.5) !important;
            }

            /* 动画 */
            @keyframes sf-bounce-in {
                0% { transform: scale(0.3); opacity: 0; }
                50% { transform: scale(1.05); }
                70% { transform: scale(0.9); }
                100% { transform: scale(1); opacity: 1; }
            }

            #sf-filter-container .sf-toggle-btn.sf-animate {
                animation: sf-bounce-in 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
            }
        `;
        document.head.appendChild(style);
    }

    // 创建界面
    function createFilterUI() {
        const container = document.createElement('div');
        container.id = 'sf-filter-container';

        container.innerHTML = `
            <div class="sf-toggle-btn sf-animate" id="sf-toggle">⚡</div>
            <div class="sf-panel" id="sf-panel">
                <div class="sf-close-btn" id="sf-close">×</div>
                <div class="sf-title">硅基流动模型筛选器</div>

                <div class="sf-form-group">
                    <label class="sf-label">关键词筛选</label>
                    <input type="text" id="sf-keywords" class="sf-input" placeholder="输入关键词,用逗号分隔,如:gpt,claude">
                </div>

                <div class="sf-form-group">
                    <label class="sf-label">免费模型</label>
                    <select id="sf-free-filter" class="sf-select">
                        <option value="all">所有模型</option>
                        <option value="free-only">仅免费模型</option>
                        <option value="paid-only">仅付费模型</option>
                    </select>
                </div>

                <div class="sf-form-group">
                    <label class="sf-label">模型类型</label>
                    <select id="sf-model-type" class="sf-select">
                        <option value="all">所有类型</option>
                        <option value="text">文本模型</option>
                        <option value="image">图像模型</option>
                        <option value="audio">音频模型</option>
                        <option value="video">视频模型</option>
                        <option value="embedding">嵌入模型</option>
                    </select>
                </div>

                <div class="sf-form-group">
                    <label class="sf-label">输出格式</label>
                    <select id="sf-output-format" class="sf-select">
                        <option value="comma">逗号分隔</option>
                        <option value="newline">换行分隔</option>
                        <option value="detailed">详细信息</option>
                    </select>
                </div>

                <button id="sf-filter-btn" class="sf-btn sf-btn-primary">筛选模型</button>

                <button id="sf-copy-btn" class="sf-btn sf-btn-success" disabled>复制结果</button>

                <div class="sf-stats">
                    找到的模型数量: <span id="sf-model-count">0</span>
                </div>

                <div class="sf-result-area">
                    <textarea id="sf-result-output" class="sf-textarea" placeholder="筛选结果将显示在这里..." readonly></textarea>
                </div>
            </div>
        `;

        document.body.appendChild(container);

        // 绑定事件
        document.getElementById('sf-toggle').addEventListener('click', togglePanel);
        document.getElementById('sf-close').addEventListener('click', togglePanel);
        document.getElementById('sf-filter-btn').addEventListener('click', filterModels);
        document.getElementById('sf-copy-btn').addEventListener('click', copyResults);

        // 实时筛选
        ['sf-keywords', 'sf-free-filter', 'sf-model-type', 'sf-output-format'].forEach(id => {
            const element = document.getElementById(id);
            if (element) {
                element.addEventListener('change', filterModels);
                if (element.type === 'text') {
                    element.addEventListener('input', filterModels);
                }
            }
        });

        // 首次提取数据
        extractModelData();
    }

    // 切换面板
    function togglePanel() {
        const panel = document.getElementById('sf-panel');
        isExpanded = !isExpanded;

        if (isExpanded) {
            panel.classList.add('expanded');
        } else {
            panel.classList.remove('expanded');
        }
    }

    // 改进的数据提取函数,专门针对Next.js格式
    function extractModelData() {
        console.log('开始提取模型数据...');
        extractedModels = [];
        try {
            // 查找包含模型数据的script标签
            const scripts = document.querySelectorAll('script');
            let modelsData = null;

            for (let script of scripts) {
                const content = script.textContent;
                if (content && content.includes('self.__next_f.push') && content.includes('chats')) {
                    // 提取JSON数据
                    const match = content.match(/self\.__next_f\.push\(\[.*?\]\)/);
                    if (match) {
                        try {
                            // 解析推送的数据
                            const pushData = match[0].replace('self.__next_f.push(', '').slice(0, -1);
                            const parsed = JSON.parse(pushData);

                            // 查找包含模型数据的部分
                            if (parsed.length > 1 && typeof parsed[1] === 'string') {
                                const jsonStr = parsed[1].replace(/^\d+:\[/, '[').replace(/\]$/, ']');
                                const dataArray = JSON.parse(jsonStr);

                                // 查找数据对象
                                if (!!dataArray[1] && !!dataArray[1][3] && !!dataArray[1][3].data) {
                                    modelsData = dataArray[1][3].data;
                                    parseModelData(modelsData);
                                } else {
                                    console.warn('未找到数据');
                                }
                            }
                        } catch (e) {
                            console.warn('解析Next.js数据时出错:', e);
                        }
                    }
                }
            }

            console.log(`最终提取到 ${extractedModels.length} 个模型`);

        } catch (error) {
            console.error('提取模型数据时出错:', error);
        }
    }

    // 解析模型数据
    function parseModelData(data) {
        const categories = [
            { key: 'chats', type: 'text' },
            { key: 'images', type: 'image' },
            { key: 'audios', type: 'audio' },
            { key: 'videos', type: 'video' },
            { key: 'embeddings', type: 'embedding' }
        ];

        categories.forEach(({ key, type }) => {
            if (data[key] && Array.isArray(data[key])) {
                console.log(`处理 ${key} 类别,找到 ${data[key].length} 个模型`);
                data[key].forEach(model => {
                    extractedModels.push({
                        ...model,
                        category: type
                    });
                });
            }
        });
    }

    // 在数据中查找模型
    function findModelsInData(data) {
        if (Array.isArray(data)) {
            data.forEach(item => findModelsInData(item));
        } else if (data && typeof data === 'object') {
            if (data.chats || data.images || data.audios || data.videos || data.embeddings) {
                parseModelData(data);
            } else {
                Object.values(data).forEach(value => {
                    if (typeof value === 'object') {
                        findModelsInData(value);
                    }
                });
            }
        }
    }

    // 判断模型是否免费
    function isModelFree(model) {
        const pricing = model.pricing || [];

        // 检查所有价格是否为0
        const allPricesZero = pricing.every(p => parseFloat(p.price) === 0);

        // 检查通用价格字段
        const generalPrice = parseFloat(model.price) || 0;
        const inputPrice = parseFloat(model.inputPrice) || 0;

        return allPricesZero && generalPrice === 0 && inputPrice === 0;
    }

    // 检查模型名称是否包含关键词
    function matchesKeywords(modelName, keywords) {
        if (!keywords.trim()) return true;

        const keywordList = keywords.split(',').map(k => k.trim().toLowerCase()).filter(k => k);
        const modelNameLower = modelName.toLowerCase();

        return keywordList.some(keyword => modelNameLower.includes(keyword));
    }

    // 筛选模型
    function filterModels() {
        console.log('开始筛选模型...');

        if (extractedModels.length === 0) {
            console.log('没有模型数据,重新提取...');
            extractModelData();
            if (extractedModels.length === 0) {
                console.warn('仍然没有找到模型数据');
                document.getElementById('sf-result-output').value = '没有找到模型数据,请刷新页面重试';
                return;
            }
        }

        const keywords = document.getElementById('sf-keywords').value;
        const freeFilter = document.getElementById('sf-free-filter').value;
        const modelType = document.getElementById('sf-model-type').value;
        const outputFormat = document.getElementById('sf-output-format').value;

        console.log('筛选参数:', { keywords, freeFilter, modelType, outputFormat });
        console.log('可用模型数量:', extractedModels.length);

        const filteredModels = extractedModels.filter(model => {
            // 类型筛选
            if (modelType !== 'all' && model.category !== modelType) {
                return false;
            }

            // 关键词筛选
            if (!matchesKeywords(model.modelName, keywords)) {
                return false;
            }

            // 免费模型筛选
            const isFree = isModelFree(model);
            if (freeFilter === 'free-only' && !isFree) {
                return false;
            }
            if (freeFilter === 'paid-only' && isFree) {
                return false;
            }

            return true;
        });

        console.log('筛选后模型数量:', filteredModels.length);

        // 按模型名称排序
        filteredModels.sort((a, b) => a.modelName.localeCompare(b.modelName));

        // 生成输出
        let output = '';
        if (outputFormat === 'detailed') {
            output = filteredModels.map(model => {
                const isFree = isModelFree(model);
                const freeStatus = isFree ? '免费' : '付费';
                return `${model.modelName} | ${freeStatus} | ${model.category}`;
            }).join('\n');
        } else {
            const modelNames = filteredModels.map(model => model.modelName);
            output = outputFormat === 'comma' ? modelNames.join(', ') : modelNames.join('\n');
        }

        document.getElementById('sf-result-output').value = output;
        document.getElementById('sf-model-count').textContent = filteredModels.length;
        document.getElementById('sf-copy-btn').disabled = filteredModels.length === 0;
    }

    // 复制结果
    function copyResults() {
        const textarea = document.getElementById('sf-result-output');
        textarea.select();
        textarea.setSelectionRange(0, 99999);

        try {
            document.execCommand('copy');

            const btn = document.getElementById('sf-copy-btn');
            const originalText = btn.textContent;
            btn.textContent = '已复制!';
            btn.style.background = 'linear-gradient(135deg, #FF9500, #FF6482)';

            setTimeout(() => {
                btn.textContent = originalText;
                btn.style.background = 'linear-gradient(135deg, #34C759, #30D158)';
            }, 2000);
        } catch (err) {
            console.error('复制失败:', err);
            alert('复制失败,请手动选择文本复制');
        }
    }

    // 初始化
    function init() {
        createStyles();
        createFilterUI();

        // 延迟提取数据,确保页面完全加载
        setTimeout(() => {
            extractModelData();
            filterModels();
        }, 3000);

        // 定期重试提取数据
        const retryInterval = setInterval(() => {
            if (extractedModels.length === 0) {
                console.log('重试提取数据...');
                extractModelData();
            } else {
                clearInterval(retryInterval);
            }
        }, 5000);
    }

    // 页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();