搜尋引擎重定向插件,搜尋引擎管理助手

將搜尋請求從一個搜尋引擎重定向到另一個(支援多個搜尋引擎),並管理搜尋引擎重定向規則

// ==UserScript==
// @name                    Search Engine Redirector, Search Engine Manager
// @name:zh-CN              搜索引擎重定向插件,搜索引擎管理助手
// @name:zh-TW              搜尋引擎重定向插件,搜尋引擎管理助手
// @name:en                 Search Engine Redirector, Search Engine Manager
// @name:ja                 検索エンジンリダイレクター・検索エンジン管理
// @name:ko                 검색 엔진 리디렉터, 검색 엔진 관리자
// @name:ru                 Перенаправление поисковых систем, Менеджер поисковых систем
// @name:fr                 Redirection de moteur de recherche, Gestionnaire de moteurs de recherche
// @name:es                 Redireccionador de motores de búsqueda, Gestor de motores de búsqueda
// @name:de                 Suchmaschinen-Umleiter, Suchmaschinen-Manager
// @name:pt-BR              Redirecionador de Mecanismos de Busca, Gerenciador de Mecanismos de Busca
// @name:it                 Reindirizzatore motore di ricerca, Gestore motore di ricerca
// @name:tr                 Arama Motoru Yönlendirici, Arama Motoru Yöneticisi
// @name:vi                 Trình chuyển hướng công cụ tìm kiếm, Trình quản lý công cụ tìm kiếm
// @name:pl                 Przekierowywacz wyszukiwarek, Menedżer wyszukiwarek
// @name:uk                 Перенаправлення пошукових систем, Менеджер пошукових систем
// @name:ar                 معيد توجيه محرك البحث، مدير محرك البحث
// @name:hi                 सर्च इंजन रीडायरेक्टर, सर्च इंजन मैनेजर
// @description             Redirect search requests from one engine to another (supports multiple engines) and manage search engine redirection rules
// @description:zh-CN       将搜索请求从一个搜索引擎重定向到另一个(支持多个搜索引擎),并管理搜索引擎重定向规则
// @description:zh-TW       將搜尋請求從一個搜尋引擎重定向到另一個(支援多個搜尋引擎),並管理搜尋引擎重定向規則
// @description:en          Redirect search requests from one engine to another (supports multiple engines) and manage search engine redirection rules
// @description:ja          検索リクエストを別の検索エンジンにリダイレクト(複数エンジン対応)、リダイレクトルールを管理
// @description:ko          검색 요청을 다른 검색 엔진으로 리디렉션(여러 엔진 지원), 검색 엔진 리디렉션 규칙 관리
// @description:ru          Перенаправляет поисковые запросы с одной системы на другую (поддержка нескольких систем) и управляет правилами перенаправления
// @description:fr          Redirige les requêtes de recherche d'un moteur à un autre (plusieurs moteurs pris en charge) et gère les règles de redirection
// @description:es          Redirige las búsquedas de un motor a otro (soporta múltiples motores) y gestiona reglas de redirección
// @description:de          Leitet Suchanfragen von einer Suchmaschine zu einer anderen um (unterstützt mehrere Suchmaschinen) und verwaltet Umleitungsregeln
// @description:pt-BR       Redireciona buscas de um mecanismo para outro (suporta múltiplos mecanismos) e gerencia regras de redirecionamento
// @description:it          Reindirizza le ricerche da un motore all'altro (supporta più motori) e gestisce le regole di reindirizzamento
// @description:tr          Arama isteklerini bir motordan diğerine yönlendirir (birden fazla motor desteklenir) ve yönlendirme kurallarını yönetir
// @description:vi          Chuyển hướng tìm kiếm từ một công cụ sang công cụ khác (hỗ trợ nhiều công cụ), quản lý quy tắc chuyển hướng
// @description:pl          Przekierowuje zapytania z jednej wyszukiwarki do innej (obsługuje wiele wyszukiwarek) i zarządza regułami przekierowań
// @description:uk          Перенаправляє пошукові запити з однієї системи на іншу (підтримка декількох систем) і керує правилами перенаправлення
// @description:ar          يعيد توجيه طلبات البحث من محرك إلى آخر (يدعم عدة محركات) ويدير قواعد إعادة التوجيه (يدعم الصينية والإنجليزية)
// @description:hi          खोज अनुरोधों को एक इंजन से दूसरे में रीडायरेक्ट करें (कई इंजन समर्थित), रीडायरेक्शन नियम प्रबंधित करें 
// @namespace               https://github.com/r6hk/search-engine-redirect/
// @homepage                https://github.com/r6hk/search-engine-redirect/
// @supportURL              https://github.com/r6hk/search-engine-redirect/
// @version                 1.0.1
// @author                  r6hk
// @match                   *://*/*
// @grant                   GM_registerMenuCommand
// @grant                   GM_setValue
// @grant                   GM_getValue
// @grant                   GM_getResourceText
// @grant                   GM_addStyle
// @run-at                  document-start
// @license                 MIT
// ==/UserScript==

(function() {
    'use strict';

    // 搜索引擎信息
    const SEARCH_ENGINES = {
        Google: {
            prefix: 'https://www.google.com/search',
            param: 'q'
        },
        Bing: {
            prefix: 'https://www.bing.com/search',
            param: 'q'
        },
        DuckDuckGo: {
            prefix: 'https://duckduckgo.com/',
            param: 'q'
        },
        Yandex: {
            prefix: 'https://yandex.com/search',
            param: 'text'
        },
        'Brave Search': {
            prefix: 'https://search.brave.com/search',
            param: 'q'
        },
        Startpage: {
            prefix: 'https://www.startpage.com/do/search',
            param: 'q'
        },
        Ecosia: {
            prefix: 'https://www.ecosia.org/search',
            param: 'q'
        }
    };

    // 存储键名
    const STORAGE_KEYS = {
        REDIRECT_LIST: 'redirect_list',
        RULES: 'rules'
    };

    // 国际化文本
    const i18n = {
        en: {
            title: "Search Engine Redirector Settings",
            redirectLabel: "Search engines I want to redirect:",
            enabledRules: "Enabled Rules",
            disabledRules: "Disabled Rules",
            addButton: "Add",
            name: "Name",
            keyword: "Keyword",
            urlFormat: "URL Format (use %s for query)",
            actions: "Actions",
            setDefault: "Set Default",
            disable: "Disable",
            enable: "Enable",
            delete: "Delete",
            save: "Save",
            cancel: "Cancel",
            addRule: "Add Rule",
            ruleName: "Rule Name",
            ruleKeyword: "Shortcut Keyword",
            ruleUrl: "URL Format",
            required: "Required",
            invalidUrl: "URL must contain '%s'",
            defaultSet: "Default set",
            ruleAdded: "Rule added",
            ruleDeleted: "Rule deleted",
            ruleEnabled: "Rule enabled",
            ruleDisabled: "Rule disabled",
            settingsSaved: "Settings saved"
        },
        zh: {
            title: "搜索引擎重定向设置",
            redirectLabel: "我希望重定向的搜索引擎:",
            enabledRules: "已启用规则",
            disabledRules: "已禁用规则",
            addButton: "添加",
            name: "名称",
            keyword: "快捷字词",
            urlFormat: "网址格式(用\"%s\"代替搜索字词)",
            actions: "操作",
            setDefault: "设为默认",
            disable: "禁用",
            enable: "启用",
            delete: "删除",
            save: "保存",
            cancel: "取消",
            addRule: "添加规则",
            ruleName: "规则名称",
            ruleKeyword: "快捷字词",
            ruleUrl: "网址格式",
            required: "必填",
            invalidUrl: "URL必须包含'%s'",
            defaultSet: "已设为默认",
            ruleAdded: "规则已添加",
            ruleDeleted: "规则已删除",
            ruleEnabled: "规则已启用",
            ruleDisabled: "规则已禁用",
            settingsSaved: "设置已保存"
        }
    };

    // 获取语言
    const lang = navigator.language.startsWith('zh') ? 'zh' : 'en';
    const text = i18n[lang];

    // 初始化存储
    function initializeStorage() {
        if (GM_getValue(STORAGE_KEYS.REDIRECT_LIST) === undefined) {
            GM_setValue(STORAGE_KEYS.REDIRECT_LIST, []);
        }

        if (GM_getValue(STORAGE_KEYS.RULES) === undefined) {
            GM_setValue(STORAGE_KEYS.RULES, [
                { id: 1, name: "Brave", keyword: "br", url: "https://search.brave.com/search?q=%s&source=desktop", enabled: true, isDefault: false },
                { id: 2, name: "DuckDuckGo", keyword: "d", url: "https://duckduckgo.com/?q=%s&t=brave", enabled: true, isDefault: false }
            ]);
        }
    }

    // 主重定向逻辑
    function performRedirect() {
        const redirectList = GM_getValue(STORAGE_KEYS.REDIRECT_LIST, []);
        const rules = GM_getValue(STORAGE_KEYS.RULES, []);
        const currentUrl = window.location.href;
        if (/([&?])redirect=false(?!\w)/.test(currentUrl)) return;
        let matchedEngine = null;
        for (const engineName of redirectList) {
            const engine = SEARCH_ENGINES[engineName];
            if (engine && currentUrl.startsWith(engine.prefix)) {
                matchedEngine = engine;
                break;
            }
        }

        if (!matchedEngine) return;

        // 提取搜索关键词
        const urlObj = new URL(currentUrl);
        let query = urlObj.searchParams.get(matchedEngine.param);
        if (!query) return;

        // 分割关键词
        const words = query.trim().split(/\s+/);
        const firstWord = words[0];
        let remainingQuery = words.slice(1).join(' ');

        // 查找匹配规则
        let matchedRule = null;
        let defaultRule = null;
        for (const rule of rules) {
            if (!rule.enabled) continue;

            if (rule.keyword === firstWord) {
                matchedRule = rule;
                break;
            }

            if (rule.isDefault) {
                defaultRule = rule;
            }
        }

        // 使用默认规则(如果存在)
        if (!matchedRule && defaultRule) {
            matchedRule = defaultRule;
            remainingQuery = query; // 使用完整查询
        }

        if (matchedRule) {
            let targetUrl = matchedRule.url.replace('%s', encodeURIComponent(remainingQuery));
            if (targetUrl.includes('?')) {
                targetUrl += '&redirect=false';
            } else {
                targetUrl += '?redirect=false';
            }
            window.location.replace(targetUrl);
        }
    }

    // 创建设置UI
    function createSettingsUI() {
        const style = `
            .redirector-settings {
                font-family: system-ui, -apple-system, sans-serif;
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0,0,0,0.7);
                display: flex;
                align-items: center;
                justify-content: center;
                z-index: 9999;
                overflow-y: auto;
                padding: 20px;
                box-sizing: border-box;
            }

            .settings-container {
                background: white;
                border-radius: 12px;
                box-shadow: 0 10px 30px rgba(0,0,0,0.2);
                width: 100%;
                max-width: 900px;
                max-height: 90vh;
                overflow-y: auto;
            }

            .settings-header {
                padding: 20px;
                border-bottom: 1px solid #eaeaea;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }

            .settings-title {
                font-size: 1.5rem;
                font-weight: 600;
                color: #333;
                margin: 0;
            }

            .close-btn {
                background: none;
                border: none;
                font-size: 1.5rem;
                cursor: pointer;
                color: #666;
                padding: 5px;
            }

            .settings-body {
                padding: 20px;
            }

            .section {
                margin-bottom: 30px;
            }

            .section-title {
                font-size: 1.2rem;
                margin: 0 0 15px 0;
                color: #444;
                font-weight: 600;
            }

            .engines-grid {
                display: grid;
                grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
                gap: 12px;
                margin-bottom: 20px;
            }

            .engine-item {
                display: flex;
                align-items: center;
            }

            .engine-item input {
                margin-right: 8px;
            }

            .rules-container {
                display: flex;
                flex-direction: column;
                gap: 20px;
            }

            .rules-table {
                width: 100%;
                border-collapse: collapse;
            }

            .rules-table th {
                background: #f8f9fa;
                text-align: left;
                padding: 12px 15px;
                font-weight: 600;
                color: #495057;
                border-bottom: 1px solid #dee2e6;
            }

            .rules-table td {
                padding: 12px 15px;
                border-bottom: 1px solid #eaeaea;
            }

            .rules-table tr:nth-child(even) {
                background-color: #f9f9f9;
            }

            .rules-table tr:hover {
                background-color: #f0f7ff;
            }

            .action-btn {
                background: none;
                border: 1px solid #d1d5db;
                border-radius: 4px;
                padding: 5px 10px;
                margin: 0 3px;
                cursor: pointer;
                font-size: 0.85rem;
                transition: all 0.2s;
            }

            .action-btn:hover {
                background: #f0f7ff;
                border-color: #3b82f6;
                color: #3b82f6;
            }

            .add-btn {
                background: #3b82f6;
                color: white;
                border: none;
                border-radius: 6px;
                padding: 8px 16px;
                cursor: pointer;
                font-weight: 500;
                display: flex;
                align-items: center;
                gap: 5px;
                margin-top: 10px;
                transition: background 0.2s;
            }

            .add-btn:hover {
                background: #2563eb;
            }

            .modal {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0,0,0,0.5);
                display: flex;
                align-items: center;
                justify-content: center;
                z-index: 10000;
            }

            .modal-content {
                background: white;
                border-radius: 10px;
                width: 90%;
                max-width: 500px;
                padding: 25px;
                box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            }

            .modal-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 20px;
            }

            .modal-title {
                font-size: 1.3rem;
                font-weight: 600;
                margin: 0;
            }

            .form-group {
                margin-bottom: 20px;
            }

            .form-label {
                display: block;
                margin-bottom: 8px;
                font-weight: 500;
                color: #444;
            }

            .form-input {
                width: 100%;
                padding: 10px 12px;
                border: 1px solid #d1d5db;
                border-radius: 6px;
                font-size: 1rem;
                box-sizing: border-box;
            }

            .form-input:focus {
                outline: none;
                border-color: #3b82f6;
                box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
            }

            .error-message {
                color: #e53e3e;
                font-size: 0.85rem;
                margin-top: 5px;
                display: none;
            }

            .modal-footer {
                display: flex;
                justify-content: flex-end;
                gap: 10px;
                margin-top: 20px;
            }

            .btn {
                padding: 10px 20px;
                border-radius: 6px;
                font-weight: 500;
                cursor: pointer;
                transition: all 0.2s;
            }

            .btn-primary {
                background: #3b82f6;
                color: white;
                border: none;
            }

            .btn-primary:hover {
                background: #2563eb;
            }

            .btn-secondary {
                background: #f3f4f6;
                color: #4b5563;
                border: 1px solid #d1d5db;
            }

            .btn-secondary:hover {
                background: #e5e7eb;
            }

            .toast {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background: #333;
                color: white;
                padding: 12px 20px;
                border-radius: 6px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                z-index: 10001;
                opacity: 0;
                transform: translateY(20px);
                transition: all 0.3s;
            }

            .toast.show {
                opacity: 1;
                transform: translateY(0);
            }
        `;

        GM_addStyle(style);

        const settingsContainer = document.createElement('div');
        settingsContainer.className = 'redirector-settings';
        settingsContainer.innerHTML = `
            <div class="settings-container">
                <div class="settings-header">
                    <h2 class="settings-title">${text.title}</h2>
                    <button class="close-btn">&times;</button>
                </div>
                <div class="settings-body">
                    <div class="section">
                        <h3 class="section-title">${text.redirectLabel}</h3>
                        <div class="engines-grid" id="engines-grid"></div>
                    </div>

                    <div class="rules-container">
                        <div class="section">
                            <div style="display: flex; justify-content: space-between; align-items: center;">
                                <h3 class="section-title">${text.enabledRules}</h3>
                                <button class="add-btn" id="add-enabled-btn">+ ${text.addButton}</button>
                            </div>
                            <table class="rules-table" id="enabled-table">
                                <thead>
                                    <tr>
                                        <th>${text.name}</th>
                                        <th>${text.keyword}</th>
                                        <th>${text.urlFormat}</th>
                                        <th>${text.actions}</th>
                                    </tr>
                                </thead>
                                <tbody id="enabled-rules-body"></tbody>
                            </table>
                        </div>

                        <div class="section">
                            <div style="display: flex; justify-content: space-between; align-items: center;">
                                <h3 class="section-title">${text.disabledRules}</h3>
                                <button class="add-btn" id="add-disabled-btn">+ ${text.addButton}</button>
                            </div>
                            <table class="rules-table" id="disabled-table">
                                <thead>
                                    <tr>
                                        <th>${text.name}</th>
                                        <th>${text.keyword}</th>
                                        <th>${text.urlFormat}</th>
                                        <th>${text.actions}</th>
                                    </tr>
                                </thead>
                                <tbody id="disabled-rules-body"></tbody>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(settingsContainer);

        // 关闭按钮
        settingsContainer.querySelector('.close-btn').addEventListener('click', () => {
            document.body.removeChild(settingsContainer);
        });

        // 渲染设置
        renderSettings();

        // 添加规则按钮
        document.getElementById('add-enabled-btn').addEventListener('click', () => {
            showAddRuleModal(true);
        });

        document.getElementById('add-disabled-btn').addEventListener('click', () => {
            showAddRuleModal(false);
        });
    }

    // 渲染设置
    function renderSettings() {
        const redirectList = GM_getValue(STORAGE_KEYS.REDIRECT_LIST, []);
        const rules = GM_getValue(STORAGE_KEYS.RULES, []);

        // 渲染搜索引擎选择
        const enginesGrid = document.getElementById('engines-grid');
        enginesGrid.innerHTML = '';

        Object.keys(SEARCH_ENGINES).forEach(engine => {
            const isChecked = redirectList.includes(engine);
            const engineItem = document.createElement('label');
            engineItem.className = 'engine-item';
            engineItem.innerHTML = `
                <input type="checkbox" value="${engine}" ${isChecked ? 'checked' : ''}>
                ${engine}
            `;
            enginesGrid.appendChild(engineItem);
        });

        // 为复选框添加事件
        enginesGrid.querySelectorAll('input').forEach(checkbox => {
            checkbox.addEventListener('change', () => {
                const newRedirectList = [...enginesGrid.querySelectorAll('input:checked')].map(cb => cb.value);
                GM_setValue(STORAGE_KEYS.REDIRECT_LIST, newRedirectList);
            });
        });

        // 渲染规则表
        renderRulesTable('enabled-rules-body', rules.filter(r => r.enabled));
        renderRulesTable('disabled-rules-body', rules.filter(r => !r.enabled));
    }

    // 渲染规则表
    function renderRulesTable(tableId, rules) {
        const tableBody = document.getElementById(tableId);
        tableBody.innerHTML = '';

        if (rules.length === 0) {
            const row = document.createElement('tr');
            row.innerHTML = `<td colspan="4" style="text-align: center; padding: 20px; color: #777;">${lang === 'zh' ? '没有规则' : 'No rules'}</td>`;
            tableBody.appendChild(row);
            return;
        }
        rules.forEach(rule => {
            const isDefault = rule.isDefault;
            const row = document.createElement('tr');
            row.innerHTML = `
                <td>${rule.name} ${isDefault ? '<span style="color:#3b82f6;font-size:0.8em;">(默认)</span>' : ''}</td>
                <td>${rule.keyword}</td>
                <td style="max-width: 300px; word-break: break-all;">${rule.url}</td>
                <td>
                    ${rule.enabled ?
                        `<button class="action-btn set-default-btn" data-id="${rule.id}">${text.setDefault}</button>
                         <button class="action-btn disable-btn" data-id="${rule.id}" ${isDefault ? 'disabled style="opacity:0.5;cursor:not-allowed;"' : ''}>${text.disable}</button>` :
                        `<button class="action-btn enable-btn" data-id="${rule.id}">${text.enable}</button>`
                    }
                    <button class="action-btn delete-btn" data-id="${rule.id}" ${isDefault ? 'disabled style="opacity:0.5;cursor:not-allowed;"' : ''}>${text.delete}</button>
                </td>
            `;
            tableBody.appendChild(row);
        });
        // 事件处理
        tableBody.querySelectorAll('.set-default-btn').forEach(btn => {
            btn.addEventListener('click', () => setDefaultRule(btn.dataset.id));
        });

        tableBody.querySelectorAll('.disable-btn').forEach(btn => {
            if (btn.hasAttribute('disabled')) return;
            btn.addEventListener('click', () => toggleRule(btn.dataset.id, false));
        });

        tableBody.querySelectorAll('.enable-btn').forEach(btn => {
            btn.addEventListener('click', () => toggleRule(btn.dataset.id, true));
        });

        tableBody.querySelectorAll('.delete-btn').forEach(btn => {
            if (btn.hasAttribute('disabled')) return;
            btn.addEventListener('click', () => deleteRule(btn.dataset.id));
        });
    }

    // 设置默认规则
    function setDefaultRule(ruleId) {
        const rules = GM_getValue(STORAGE_KEYS.RULES, []);
        const updatedRules = rules.map(rule => ({
            ...rule,
            isDefault: rule.id.toString() === ruleId
        }));

        GM_setValue(STORAGE_KEYS.RULES, updatedRules);
        renderSettings();
        showToast(text.defaultSet);
    }

    // 切换规则状态
    function toggleRule(ruleId, enabled) {
        const rules = GM_getValue(STORAGE_KEYS.RULES, []);
        const updatedRules = rules.map(rule =>
            rule.id.toString() === ruleId ? {...rule, enabled} : rule
        );

        GM_setValue(STORAGE_KEYS.RULES, updatedRules);
        renderSettings();
        showToast(enabled ? text.ruleEnabled : text.ruleDisabled);
    }

    // 删除规则
    function deleteRule(ruleId) {
        const rules = GM_getValue(STORAGE_KEYS.RULES, []);
        const updatedRules = rules.filter(rule => rule.id.toString() !== ruleId);

        GM_setValue(STORAGE_KEYS.RULES, updatedRules);
        renderSettings();
        showToast(text.ruleDeleted);
    }

    // 显示添加规则模态框
    function showAddRuleModal(enabled) {
        const modal = document.createElement('div');
        modal.className = 'modal';
        modal.innerHTML = `
            <div class="modal-content">
                <div class="modal-header">
                    <h3 class="modal-title">${text.addRule}</h3>
                    <button class="close-btn">&times;</button>
                </div>
                <div class="form-group">
                    <label class="form-label">${text.ruleName} <span style="color:red">*</span></label>
                    <input type="text" class="form-input" id="rule-name">
                    <div class="error-message" id="name-error">${text.required}</div>
                </div>
                <div class="form-group">
                    <label class="form-label">${text.ruleKeyword} <span style="color:red">*</span></label>
                    <input type="text" class="form-input" id="rule-keyword">
                    <div class="error-message" id="keyword-error">${text.required}</div>
                </div>
                <div class="form-group">
                    <label class="form-label">${text.ruleUrl} <span style="color:red">*</span></label>
                    <input type="text" class="form-input" id="rule-url" placeholder="https://example.com/search?q=%s">
                    <div class="error-message" id="url-error">${text.invalidUrl.replace('%s', '%s')}</div>
                </div>
                <div class="modal-footer">
                    <button class="btn btn-secondary" id="cancel-btn">${text.cancel}</button>
                    <button class="btn btn-primary" id="save-btn">${text.save}</button>
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        // 关闭按钮
        modal.querySelector('.close-btn').addEventListener('click', () => {
            document.body.removeChild(modal);
        });

        // 取消按钮
        modal.querySelector('#cancel-btn').addEventListener('click', () => {
            document.body.removeChild(modal);
        });

        // 保存按钮
        modal.querySelector('#save-btn').addEventListener('click', () => {
            const name = modal.querySelector('#rule-name').value.trim();
            const keyword = modal.querySelector('#rule-keyword').value.trim();
            const url = modal.querySelector('#rule-url').value.trim();

            // 验证输入
            let valid = true;

            if (!name) {
                modal.querySelector('#name-error').style.display = 'block';
                valid = false;
            } else {
                modal.querySelector('#name-error').style.display = 'none';
            }

            if (!keyword) {
                modal.querySelector('#keyword-error').style.display = 'block';
                valid = false;
            } else {
                modal.querySelector('#keyword-error').style.display = 'none';
            }

            if (!url || !url.includes('%s')) {
                modal.querySelector('#url-error').style.display = 'block';
                valid = false;
            } else {
                modal.querySelector('#url-error').style.display = 'none';
            }

            if (!valid) return;

            // 保存规则
            const rules = GM_getValue(STORAGE_KEYS.RULES, []);
            const newId = rules.length > 0 ? Math.max(...rules.map(r => r.id)) + 1 : 1;

            rules.push({
                id: newId,
                name,
                keyword,
                url,
                enabled,
                isDefault: false
            });

            GM_setValue(STORAGE_KEYS.RULES, rules);
            document.body.removeChild(modal);
            renderSettings();
            showToast(text.ruleAdded);
        });
    }

    // 显示提示消息
    function showToast(message) {
        let toast = document.querySelector('.toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.className = 'toast';
            document.body.appendChild(toast);
        }

        toast.textContent = message;
        toast.classList.add('show');

        setTimeout(() => {
            toast.classList.remove('show');
        }, 3000);
    }

    // 初始化
    initializeStorage();

    // 注册菜单命令
    GM_registerMenuCommand(text.title, createSettingsUI);

    // 执行重定向
    performRedirect();
})();