Greasy Fork镜像 还支持 简体中文。

搜索引擎增强

搜索引擎导航增强,支持拖拽、缩放、折叠和状态记忆,并集成硅基流动 AI 回答

// ==UserScript==
// @name              搜索引擎增强
// @namespace         search_enhance_namespace
// @version           4.4.0
// @description       搜索引擎导航增强,支持拖拽、缩放、折叠和状态记忆,并集成硅基流动 AI 回答
// @author            zyh
// @match             *://www.baidu.com/*
// @match             *://www.so.com/s*
// @match             *://www.sogou.com/web*
// @match             *://cn.bing.com/search*
// @match             *://www.bing.com/search*
// @match             *://www.google.com/search*
// @match             *://www.google.com.hk/search*
// @grant             GM_getValue
// @grant             GM_setValue
// @grant             GM_xmlhttpRequest
// @connect           api.siliconflow.cn
// @license           MIT
// ==/UserScript==

(function() {
  'use strict';

  class SearchEnhancer {
    constructor() {
      this.host = window.location.host;
      this.initData();
      this.initApiConfig();

      this.engineConfig = this.searchEnginesData.find(engine => this.host.includes(engine.host));
      if (!this.engineConfig) {
        return;
      }

      this.waitForElement(this.engineConfig.elementInput, () => {
        this.settings = this.loadSettings();
        this.lastExpandedHeight = this.settings.height > 40 ? this.settings.height : 400;
        this.initUI();
        if (this.apiSettings.autoAsk) {
          this.ensureAutoWatcher();
          this.askAi('auto');
        }
      });
    }

    initData() {
      this.searchEnginesData = [
        { host: 'baidu.com', name: '百度', elementInput: '#kw' },
        { host: 'so.com', name: '360搜索', elementInput: '#keyword' },
        { host: 'sogou.com', name: '搜狗', elementInput: '#upquery' },
        { host: 'bing.com', name: '必应', elementInput: '#sb_form_q' },
        { host: 'google.com', name: '谷歌', elementInput: "input[name='q'],textarea[name='q']" }
      ];
      const defaults = [
        {
          name: '搜索引擎',
          list: [
            { name: '百度', url: 'https://www.baidu.com/s?wd=@@', icon: 'https://www.google.com/s2/favicons?domain=baidu.com&sz=16' },
            { name: '必应', url: 'https://cn.bing.com/search?q=@@', icon: 'https://www.google.com/s2/favicons?domain=bing.com&sz=16' },
            { name: 'Google', url: 'https://www.google.com/search?q=@@', icon: 'https://www.google.com/s2/favicons?domain=google.com&sz=16' }
          ]
        },
        {
          name: '综合搜索',
          list: [
            { name: '知乎', url: 'https://www.zhihu.com/search?q=@@', icon: 'https://www.google.com/s2/favicons?domain=zhihu.com&sz=16' },
            { name: 'CSDN', url: 'https://so.csdn.net/so/search?q=@@', icon: 'https://www.google.com/s2/favicons?domain=csdn.net&sz=16' },
            { name: 'GitHub', url: 'https://github.com/search?q=@@', icon: 'https://www.google.com/s2/favicons?domain=github.com&sz=16' },
            { name: '小红书', url: 'https://www.xiaohongshu.com/search_result?keyword=@@', icon: 'https://bu.dusays.com/2025/10/05/68e1ea7572ba0.png' },


          ]
        },
        {
          name: '视频搜索',
          list: [
            { name: 'B站', url: 'https://search.bilibili.com/all?keyword=@@', icon: 'https://www.google.com/s2/favicons?domain=bilibili.com&sz=16' },
            { name: '抖音', url: 'https://www.douyin.com/search/@@', icon: 'https://bu.dusays.com/2025/10/05/68e1eac4790de.png' },
            { name: 'YouTube', url: 'https://www.youtube.com/results?search_query=@@', icon: 'https://www.google.com/s2/favicons?domain=youtube.com&sz=16' }
          ]
        },
        {
          name: '学术搜索',
          list: [
            { name: '谷粉学术', url: 'https://www.defineabc.com/scholar?hl=en&q=@@', icon: 'https://www.google.com/s2/favicons?domain=defineabc.com&sz=16' },
            { name: 'Aminer', url: 'https://www.aminer.cn/search?t=b&q=@@', icon: 'https://www.google.com/s2/favicons?domain=aminer.cn&sz=16' }
          ]
        }
      ];
      this.navigationStorageKey = 'enhancer_nav_data_v1';
      this.defaultNavigationData = defaults;
      this.navigationData = this.loadNavigationData();
    }

    initApiConfig() {
      this.apiConfig = {
        baseUrl: 'https://api.siliconflow.cn/v1',
        chatPath: '/chat/completions',
        defaultModel: 'moonshotai/Kimi-K2-Instruct-0905',
        models: [
          { value: 'moonshotai/Kimi-K2-Instruct-0905', label: 'Kimi-K2-Instruct-0905' },
          { value: 'Qwen/Qwen3-32B', label: 'Qwen3-32B' },
          { value: 'deepseek-ai/DeepSeek-R1', label: 'DeepSeek-R1' },
          { value: 'Qwen/Qwen2.5-72B-Instruct', label: 'Qwen2.5-72B' }
        ]
      };
      this.apiSettings = {
        apiKey: GM_getValue('siliconflow_api_key', ''),
        model: GM_getValue('siliconflow_last_model', this.apiConfig.defaultModel),
        autoAsk: GM_getValue('siliconflow_auto_ask', false)
      };
      if (!this.apiConfig.models.some(item => item.value === this.apiSettings.model)) {
        this.apiSettings.model = this.apiConfig.defaultModel;
      }
      // 清理旧的AI折叠状态,强制默认为搜索页面
      GM_setValue('siliconflow_ai_collapsed', undefined);
      this.uiState = {
        currentPage: GM_getValue('siliconflow_current_page', 'search'),
        showKeyEditor: !this.apiSettings.apiKey
      };
      // 确保默认始终是搜索页面
      if (this.uiState.currentPage !== 'search' && this.uiState.currentPage !== 'ai') {
        this.uiState.currentPage = 'search';
        GM_setValue('siliconflow_current_page', 'search');
      }
      this.lastAnswerQuery = '';
      this.isRequesting = false;
      this.queryWatcher = null;
      this.aiElements = null;
    }

    initUI() {
      if (document.getElementById('search-enhancer-panel')) return;
      this.createPanel();
      this.applyStyles();
      this.attachEventListeners();
      if (!this.apiSettings.apiKey) {
        this.updateAiStatus('配置 SiliconFlow API Key 后即可调用 AI', 'warn');
      } else if (this.apiSettings.autoAsk) {
        this.updateAiStatus('自动回答已开启', 'info');
      } else {
        this.updateAiStatus('可手动生成 AI 回答', 'info');
      }
    }

    loadSettings() {
      const defaults = { x: 20, y: 120, width: 220, height: 120, isCollapsed: false };
      const saved = GM_getValue(`enhancer_settings_${this.host}`, {});
      return { ...defaults, ...saved };
    }

    saveSettings() {
      if (!this.panel) return;
      const currentSettings = {
        x: this.panel.offsetLeft,
        y: this.panel.offsetTop,
        width: this.panel.offsetWidth,
        height: this.lastExpandedHeight,
        isCollapsed: this.panel.classList.contains('collapsed')
      };
      GM_setValue(`enhancer_settings_${this.host}`, currentSettings);
    }

    createPanel() {
      const navHtml = this.navigationData
        .map(cat => {
          const links = cat.list
            .map(item => `<a href="#" data-url="${item.url}"><img src="${item.icon}" class="nav-icon" alt="${item.name}" onerror="this.style.display='none'" />${item.name}</a>`)
            .join('');
          return `<div class="nav-section"><div class="section-title">${cat.name}</div><div class="nav-links">${links}</div></div>`;
        })
        .join('');

      const modelOptions = this.apiConfig.models
        .map(item => `<option value="${item.value}">${item.label}</option>`)
        .join('');

      const shouldShowEditor = this.uiState.showKeyEditor || !this.apiSettings.apiKey;
      const shouldShowStatus = this.apiSettings.apiKey && !shouldShowEditor;
      const manageBtnVisible = this.apiSettings.apiKey || !shouldShowEditor;
      const manageBtnLabel = shouldShowEditor ? '取消编辑' : (this.apiSettings.apiKey ? '重新配置密钥' : '配置密钥');

      const searchPageHtml = `
        <div class="page-content search-page ${this.uiState.currentPage === 'search' ? 'active' : ''}">
          ${navHtml}
        </div>
      `;

      const aiPageHtml = `
        <div class="page-content ai-page ${this.uiState.currentPage === 'ai' ? 'active' : ''}">
          <div class="ai-key-row${shouldShowEditor ? '' : ' hidden'}">
            <input type="password" class="ai-key-input" placeholder="粘贴 SiliconFlow API Key" autocomplete="off" />
            <button type="button" class="ai-save-btn secondary">保存密钥</button>
          </div>
          <div class="ai-key-status${shouldShowStatus ? '' : ' hidden'}">密钥已安全保存,默认仅在本地可见。</div>
          <div class="ai-controls">
            <select class="ai-model-select ai-option-select">${modelOptions}</select>
            <button type="button" class="ai-ask-btn">快速回答</button>
            <label class="ai-auto-label"><input type="checkbox" class="ai-auto-toggle" /> 自动回答</label>
          </div>
          <div class="ai-status"></div>
          <div class="ai-answer"></div>
          <div class="ai-manage-section">
            <button type="button" class="ai-manage-key-btn secondary${manageBtnVisible ? '' : ' hidden'}">${manageBtnLabel}</button>
          </div>
        </div>
      `;

      const panelHtml = `
        <div class="nav-header">
          <div class="page-tabs">
            <button type="button" class="page-tab ${this.uiState.currentPage === 'search' ? 'active' : ''}" data-page="search">搜索</button>
            <button type="button" class="page-tab ${this.uiState.currentPage === 'ai' ? 'active' : ''}" data-page="ai">AI</button>
            <button type="button" class="page-tab ${this.uiState.currentPage === 'settings' ? 'active' : ''}" data-page="settings">设置</button>
          </div>
          <button type="button" class="nav-toggle-btn">收起</button>
        </div>
        <div class="nav-content">${searchPageHtml}${aiPageHtml}<div class="page-content settings-page ${this.uiState.currentPage === 'settings' ? 'active' : ''}"></div></div>
        <div class="resize-handle"></div>
      `;

      this.panel = document.createElement('div');
      this.panel.id = 'search-enhancer-panel';
      this.panel.innerHTML = panelHtml;
      document.body.appendChild(this.panel);
      // 渲染设置页
      this.renderSettingsPage?.();

      if (this.settings.isCollapsed) {
        this.panel.classList.add('collapsed');
        this.panel.querySelector('.nav-content').style.display = 'none';
        this.panel.querySelector('.nav-toggle-btn').textContent = '展开';
        this.panel.style.height = 'auto';
      }
    }

    applyStyles() {
      const s = this.settings;
      const css = `
        #search-enhancer-panel {
          position: fixed;
          top: ${s.y}px;
          left: ${s.x}px;
          width: ${s.width}px;
          height: ${s.isCollapsed ? 'auto' : `${s.height}px`};
          min-width: 260px;
          min-height: 48px;
          z-index: 999999;
          display: flex;
          flex-direction: column;
          background: rgba(255, 255, 255, 0.4);
          border-radius: 12px;
          box-shadow: 0 5px 20px rgba(0, 0, 0, 0.12);
          backdrop-filter: blur(10px);
          border: 1px solid rgba(0, 0, 0, 0.08);
          user-select: none;
          overflow: hidden;
          transition: height 0.2s ease-in-out;
        }
        #search-enhancer-panel.no-transition {
          transition: none !important;
        }
        #search-enhancer-panel.collapsed {
          height: auto !important;
        }
        .nav-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 8px 14px;
          background: rgba(0, 0, 0, 0.04);
          cursor: move;
          flex-shrink: 0;
        }
        .page-tabs {
          display: flex;
          gap: 4px;
        }
        .page-tab {
          border: none;
          background: rgba(255, 255, 255, 0.6);
          cursor: pointer;
          font-size: 13px;
          color: #666;
          padding: 6px 12px;
          border-radius: 6px;
          transition: all 0.2s;
          font-weight: 500;
        }
        .page-tab:hover {
          background: rgba(255, 255, 255, 0.8);
        }
        .page-tab.active {
          background: #007bff;
          color: #fff;
          font-weight: 600;
        }
        .nav-toggle-btn {
          border: none;
          background: none;
          cursor: pointer;
          font-size: 14px;
          color: #4a4a4a;
          padding: 4px 8px;
          border-radius: 6px;
          transition: background 0.2s;
        }
        .nav-toggle-btn:hover {
          background: rgba(0, 0, 0, 0.08);
        }
        .nav-content {
          padding: 12px 15px 16px;
          overflow-y: auto;
          flex-grow: 1;
          position: relative;
        }
        .page-content {
          display: none !important;
        }
        .page-content.active {
          display: block !important;
        }
        .page-content.active.ai-page {
          display: flex !important;
          flex-direction: column;
          gap: 12px;
        }
        .nav-section {
          margin-bottom: 14px;
        }
        .section-title {
          font-size: 13px;
          font-weight: 500;
          color: #666;
          margin-bottom: 8px;
          padding-bottom: 4px;
          border-bottom: 1px solid #eee;
        }
        .nav-links {
          display: flex;
          flex-wrap: wrap;
          gap: 8px;
        }
        .nav-links a {
          padding: 4px 9px;
          color: #333;
          text-decoration: none;
          font-size: 13px;
          background: #f1f1f1;
          border-radius: 6px;
          transition: all 0.2s;
          display: inline-flex;
          align-items: center;
          gap: 4px;
        }
        .nav-links a:hover {
          background: #007bff;
          color: #fff;
          transform: translateY(-1px);
        }
        .nav-icon {
          width: 16px;
          height: 16px;
          object-fit: contain;
          flex-shrink: 0;
        }
        .resize-handle {
          position: absolute;
          bottom: 0;
          right: 0;
          width: 16px;
          height: 16px;
          cursor: se-resize;
          z-index: 10;
        }
        .ai-key-row {
          display: flex;
          align-items: center;
          gap: 8px;
          flex-wrap: wrap;
        }
        .ai-controls {
          display: flex;
          align-items: center;
          gap: 8px;
          flex-wrap: wrap;
        }
        .ai-controls input, .ai-key-row input {
          flex: 1 1 150px;
          padding: 5px 8px;
          border-radius: 6px;
          border: 1px solid #cbd5e1;
          font-size: 12px;
          outline: none;
          background: rgba(255, 255, 255, 0.85);
        }
        .ai-controls input:focus, .ai-key-row input:focus {
          border-color: #007bff;
          box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.16);
        }
        .ai-controls button, .ai-key-row button, .ai-manage-section button {
          border: none;
          background: #007bff;
          color: #fff;
          padding: 6px 10px;
          border-radius: 6px;
          cursor: pointer;
          font-size: 12px;
          transition: all 0.2s;
        }
        .ai-controls button:hover:not(:disabled), .ai-key-row button:hover:not(:disabled), .ai-manage-section button:hover:not(:disabled) {
          filter: brightness(1.05);
          transform: translateY(-1px);
        }
        .ai-controls button:disabled, .ai-key-row button:disabled {
          opacity: 0.6;
          cursor: not-allowed;
        }
        .ai-controls button.secondary, .ai-key-row button.secondary, .ai-manage-section button.secondary {
          background: #6c757d;
        }
        .ai-option-select {
          flex: 0 1 160px;
          padding: 5px 8px;
          border-radius: 6px;
          border: 1px solid #cbd5e1;
          font-size: 12px;
          background: rgba(255, 255, 255, 0.85);
        }
.ai-status {
  font-size: 12px;
  color: #555;
  min-height: 18px;
  margin-top: 4px;
  padding: 4px 6px;
  border-radius: 4px;
  background: rgba(0,0,0,0.03);
}

        .ai-key-status {
          font-size: 12px;
          color: #444;
          background: rgba(0, 0, 0, 0.04);
          border: 1px solid rgba(0, 0, 0, 0.05);
          padding: 6px 8px;
          border-radius: 6px;
        }
        .ai-status {
          font-size: 12px;
          color: #555;
          min-height: 16px;
        }
        .ai-status[data-type='error'] {
          color: #d1495b;
        }
        .ai-status[data-type='warn'] {
          color: #f4a261;
        }
        .ai-status[data-type='success'] {
          color: #2a9d8f;
        }
        .ai-answer {
          font-size: 13px;
          line-height: 1.6;
          background: rgba(255, 255, 255, 0.8);
          border: 1px solid rgba(0, 0, 0, 0.06);
          border-radius: 8px;
          padding: 10px;
          max-height: 220px;
          overflow-y: auto;
          white-space: pre-wrap;
        }
        .ai-manage-section {
          display: flex;
          justify-content: center;
          margin-top: 8px;
        }
        .hidden {
          display: none !important;
        }
        /* 设置页样式 */
        .settings-page {
          display: none !important;
        }
        .page-content.active.settings-page {
          display: block !important;
        }
        .settings-form {
          display: flex;
          flex-wrap: wrap;
          gap: 8px;
          align-items: center;
          background: rgba(255, 255, 255, 0.7);
          border: 1px solid rgba(0, 0, 0, 0.08);
          border-radius: 8px;
          padding: 10px;
          margin-bottom: 10px;
        }
        .settings-form input, .settings-form select {
          padding: 6px 8px;
          border-radius: 6px;
          border: 1px solid #cbd5e1;
          font-size: 12px;
          background: rgba(255, 255, 255, 0.9);
        }
        .settings-form .btn {
          border: none;
          background: #007bff;
          color: #fff;
          padding: 6px 10px;
          border-radius: 6px;
          cursor: pointer;
          font-size: 12px;
        }
        /* 通用按钮样式,设置页内都适用 */
        .settings-page .btn {
          border: none;
          background: #007bff;
          color: #fff;
          padding: 6px 10px;
          border-radius: 6px;
          cursor: pointer;
          font-size: 12px;
        }
        .settings-page .btn.secondary { background: #6c757d; }
        /* 删除按钮颜色与其他一致,不使用红色 */
        .settings-page .btn.danger { background: #007bff; }
        .settings-list {
          display: flex;
          flex-direction: column;
          gap: 10px;
        }
        .settings-cat { border-top: 1px solid #eee; padding-top: 6px; }
        .settings-cat-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
        .settings-cat-title { font-size: 13px; color: #666; }
        .settings-cat-actions .btn { margin-left: 6px; }
        .settings-item {
          display: grid;
          grid-template-columns: 1fr 2fr 1.2fr auto;
          gap: 6px;
          align-items: center;
          padding: 6px;
          border: 1px dashed #e5e7eb;
          border-radius: 6px;
          background: rgba(255,255,255,0.6);
        }
        .settings-item input { width: 100%; }
        .settings-actions button { margin-left: 6px; }
      `;
      const styleEl = document.createElement('style');
      styleEl.textContent = css;
      document.head.appendChild(styleEl);
    }

    attachEventListeners() {
      const header = this.panel.querySelector('.nav-header');
      const toggleBtn = this.panel.querySelector('.nav-toggle-btn');
      const resizeHandle = this.panel.querySelector('.resize-handle');
      const pageTabs = this.panel.querySelectorAll('.page-tab');

      // 页面切换功能
      pageTabs.forEach(tab => {
        tab.addEventListener('click', e => {
          e.stopPropagation();
          const targetPage = e.currentTarget.dataset.page;
          this.switchPage(targetPage);
        });
      });

      toggleBtn.addEventListener('click', e => {
        e.stopPropagation();
        const isCollapsed = this.panel.classList.toggle('collapsed');
        const contentEl = this.panel.querySelector('.nav-content');
        contentEl.style.display = isCollapsed ? 'none' : 'block';
        toggleBtn.textContent = isCollapsed ? '展开' : '收起';
        if (isCollapsed) {
          this.panel.style.height = 'auto';
        } else {
          this.panel.style.height = `${this.lastExpandedHeight}px`;
        }
        this.saveSettings();
      });

      this.bindNavLinkEvents();

      const dragOrResize = (e, type) => {
        e.preventDefault();
        this.panel.classList.add('no-transition');

        const startX = e.clientX;
        const startY = e.clientY;
        const initialX = this.panel.offsetLeft;
        const initialY = this.panel.offsetTop;
        const initialW = this.panel.offsetWidth;
        const initialH = this.panel.offsetHeight;
        let animationFrameId = null;

        const onMove = moveEvent => {
          if (animationFrameId) {
            cancelAnimationFrame(animationFrameId);
          }
          animationFrameId = requestAnimationFrame(() => {
            const dx = moveEvent.clientX - startX;
            const dy = moveEvent.clientY - startY;
            if (type === 'drag') {
              const newX = Math.max(0, Math.min(window.innerWidth - this.panel.offsetWidth, initialX + dx));
              const newY = Math.max(0, Math.min(window.innerHeight - this.panel.offsetHeight, initialY + dy));
              this.panel.style.left = `${newX}px`;
              this.panel.style.top = `${newY}px`;
            } else {
              const newW = Math.max(240, initialW + dx);
              const newH = Math.max(160, initialH + dy);
              this.panel.style.width = `${newW}px`;
              if (!this.panel.classList.contains('collapsed')) {
                this.panel.style.height = `${newH}px`;
              }
            }
          });
        };

        const onEnd = () => {
          if (animationFrameId) {
            cancelAnimationFrame(animationFrameId);
          }
          if (!this.panel.classList.contains('collapsed')) {
            this.lastExpandedHeight = this.panel.offsetHeight;
          }
          this.saveSettings();
          document.removeEventListener('mousemove', onMove);
          document.removeEventListener('mouseup', onEnd);
          this.panel.classList.remove('no-transition');
        };

        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onEnd);
      };

      header.addEventListener('mousedown', e => {
        // 只有在点击头部空白区域时才允许拖拽,避免点击标签时误触发
        if (e.target === header) {
          dragOrResize(e, 'drag');
        }
      });
      resizeHandle.addEventListener('mousedown', e => dragOrResize(e, 'resize'));

      this.aiElements = {
        page: this.panel.querySelector('.ai-page'),
        answer: this.panel.querySelector('.ai-answer'),
        status: this.panel.querySelector('.ai-status'),
        keyInput: this.panel.querySelector('.ai-key-input'),
        keyRow: this.panel.querySelector('.ai-key-row'),
        keyStatus: this.panel.querySelector('.ai-key-status'),
        saveBtn: this.panel.querySelector('.ai-save-btn'),
        askBtn: this.panel.querySelector('.ai-ask-btn'),
        autoToggle: this.panel.querySelector('.ai-auto-toggle'),
        modelSelect: this.panel.querySelector('.ai-model-select'),
        manageKeyBtn: this.panel.querySelector('.ai-manage-key-btn')
      };

      if (this.aiElements.keyInput) {
        this.aiElements.keyInput.value = '';
      }
      if (this.aiElements.autoToggle) {
        this.aiElements.autoToggle.checked = Boolean(this.apiSettings.autoAsk);
      }
      if (this.aiElements.modelSelect) {
        this.aiElements.modelSelect.value = this.apiSettings.model;
      }

      this.refreshKeyUi();

      // 强制确保正确的页面显示状态
      this.forcePageDisplay();

      this.aiElements.saveBtn?.addEventListener('click', () => {
        const value = this.aiElements.keyInput.value.trim();
        this.persistApiKey(value);
      });

      this.aiElements.keyInput?.addEventListener('keydown', e => {
        if (e.key === 'Enter') {
          e.preventDefault();
          const value = this.aiElements.keyInput.value.trim();
          this.persistApiKey(value);
        }
      });

      this.aiElements.modelSelect?.addEventListener('change', e => {
        this.persistModelValue(e.target.value);
      });

      this.aiElements.askBtn?.addEventListener('click', () => {
        this.askAi('manual');
      });

      this.aiElements.autoToggle?.addEventListener('change', e => {
        this.setAutoAsk(e.target.checked);
      });

      this.aiElements.manageKeyBtn?.addEventListener('click', () => {
        const hasKey = Boolean(this.apiSettings.apiKey);
        if (!hasKey && this.uiState.showKeyEditor) {
          this.uiState.showKeyEditor = false;
        } else {
          this.uiState.showKeyEditor = !this.uiState.showKeyEditor;
        }
        if (this.uiState.showKeyEditor && this.aiElements.keyInput) {
          this.aiElements.keyInput.value = '';
          this.updateAiStatus('请输入新的 API Key 并点击保存', 'info');
        }
        this.refreshKeyUi();
        if (this.uiState.showKeyEditor) {
          this.aiElements.keyInput?.focus();
        }
      });

      // 设置页交互
      this.attachSettingsHandlers?.();
    }

    bindNavLinkEvents() {
      this.panel.querySelectorAll('.nav-links a').forEach(link => {
        link.addEventListener('click', e => {
          e.preventDefault();
          const keyword = this.getSearchKeyword();
          const templateUrl = e.currentTarget.dataset.url;
          const targetUrl = templateUrl.replace('@@', encodeURIComponent(keyword));
          window.open(targetUrl, '_blank');
        });
      });
    }

    getSearchKeyword() {
      const input = document.querySelector(this.engineConfig.elementInput);
      if (!input) return '';
      const value = input.value || input.getAttribute('value') || input.defaultValue || '';
      return value.trim();
    }

    switchPage(targetPage) {
      if (this.uiState.currentPage === targetPage) return;

      this.uiState.currentPage = targetPage;
      GM_setValue('siliconflow_current_page', targetPage);

      // 更新页面标签状态
      this.panel.querySelectorAll('.page-tab').forEach(tab => {
        tab.classList.toggle('active', tab.dataset.page === targetPage);
      });

      // 更新页面内容显示
      this.panel.querySelectorAll('.page-content').forEach(page => {
        page.classList.toggle('active', page.classList.contains(`${targetPage}-page`));
      });
    }

    forcePageDisplay() {
      // 强制更新页面显示状态,确保与currentPage一致
      this.panel.querySelectorAll('.page-tab').forEach(tab => {
        tab.classList.toggle('active', tab.dataset.page === this.uiState.currentPage);
      });

      this.panel.querySelectorAll('.page-content').forEach(page => {
        page.classList.toggle('active', page.classList.contains(`${this.uiState.currentPage}-page`));
      });
    }

    // ----- 搜索导航数据:加载/保存/渲染 -----
    loadNavigationData() {
      const saved = GM_getValue(this.navigationStorageKey, null);
      if (Array.isArray(saved)) return saved;
      GM_setValue(this.navigationStorageKey, this.defaultNavigationData);
      return this.defaultNavigationData;
    }

    saveNavigationData() {
      GM_setValue(this.navigationStorageKey, this.navigationData);
      this.renderSearchPageNav();
      this.renderSettingsPage();
      this.attachSettingsHandlers();
    }

    renderSearchPageNav() {
      const container = this.panel.querySelector('.search-page');
      if (!container) return;
      const navHtml = this.navigationData
        .map(cat => {
          const links = cat.list
            .map(item => `<a href="#" data-url="${item.url}"><img src="${item.icon}" class="nav-icon" alt="${item.name}" onerror="this.style.display='none'" />${item.name}</a>`)
            .join('');
          return `<div class="nav-section"><div class="section-title">${cat.name}</div><div class="nav-links">${links}</div></div>`;
        })
        .join('');
      container.innerHTML = navHtml;
      this.bindNavLinkEvents();
    }

    renderSettingsPage() {
      const page = this.panel.querySelector('.settings-page');
      if (!page) return;

      const catOptions = this.navigationData
        .map((c, i) => `<option value="${i}">${c.name}</option>`)
        .join('');
      const formHtml = `
        <div class="settings-form">
          <label>分组:</label>
          <select class="set-cat-select">${catOptions}<option value="__new__">+ 新建分组</option></select>
          <input type="text" class="set-new-cat hidden" placeholder="新分组名" />
          <input type="text" class="set-name" placeholder="名称" />
          <input type="text" class="set-url" placeholder="URL(使用 @@ 作为关键词占位符)" />
          <input type="text" class="set-icon" placeholder="图标URL(可留空)" />
          <button type="button" class="btn set-save">添加</button>
          <button type="button" class="btn secondary set-cancel hidden">取消</button>
        </div>`;

      const listHtml = this.navigationData
        .map((cat, ci) => {
          const items = cat.list
            .map((it, ii) => `
              <div class="settings-item" data-ci="${ci}" data-ii="${ii}">
                <input type="text" class="si-name" value="${it.name}" />
                <input type="text" class="si-url" value="${it.url}" />
                <input type="text" class="si-icon" value="${it.icon || ''}" />
                <div class="settings-actions">
                  <button type="button" class="btn secondary si-update">保存</button>
                  <button type="button" class="btn secondary si-delete">删除</button>
                </div>
              </div>`)
            .join('');
          return `<div class="settings-cat" data-ci="${ci}">
            <div class="settings-cat-header">
              <div class="settings-cat-title">${cat.name}</div>
              <div class="settings-cat-actions">
                <button type="button" class="btn secondary cat-delete">删除分组</button>
              </div>
            </div>
            <div class="settings-list">${items || '<div style=\"color:#888;font-size:12px;\">暂无条目</div>'}</div>
          </div>`;
        })
        .join('');

      page.innerHTML = formHtml + listHtml;
    }

    attachSettingsHandlers() {
      const page = this.panel.querySelector('.settings-page');
      if (!page) return;

      const select = page.querySelector('.set-cat-select');
      const newCatInput = page.querySelector('.set-new-cat');
      const nameInput = page.querySelector('.set-name');
      const urlInput = page.querySelector('.set-url');
      const iconInput = page.querySelector('.set-icon');
      const saveBtn = page.querySelector('.set-save');
      const cancelBtn = page.querySelector('.set-cancel');

      let editing = null; // {ci, ii}

      const resetForm = () => {
        editing = null;
        nameInput.value = '';
        urlInput.value = '';
        iconInput.value = '';
        cancelBtn.classList.add('hidden');
        saveBtn.textContent = '添加';
      };

      select.addEventListener('change', () => {
        const isNew = select.value === '__new__';
        newCatInput.classList.toggle('hidden', !isNew);
      });

      saveBtn.addEventListener('click', () => {
        const isNewCat = select.value === '__new__';
        const name = nameInput.value.trim();
        const url = urlInput.value.trim();
        const icon = iconInput.value.trim();
        if (!name || !url || !url.includes('@@')) {
          alert('请填写名称与URL,且URL需要包含 @@ 作为关键词占位符');
          return;
        }

        if (editing) {
          const { ci, ii } = editing;
          this.navigationData[ci].list[ii] = { name, url, icon };
          this.saveNavigationData();
          resetForm();
          return;
        }

        let targetCatIndex;
        if (isNewCat) {
          const catName = newCatInput.value.trim();
          if (!catName) {
            alert('请输入新分组名');
            return;
          }
          targetCatIndex = this.navigationData.length;
          this.navigationData.push({ name: catName, list: [] });
        } else {
          targetCatIndex = parseInt(select.value, 10);
        }
        this.navigationData[targetCatIndex].list.push({ name, url, icon });
        this.saveNavigationData();
        this.renderSettingsPage();
        this.attachSettingsHandlers();
        resetForm();
      });

      cancelBtn.addEventListener('click', () => resetForm());

      page.querySelectorAll('.si-update').forEach(btn => {
        btn.addEventListener('click', e => {
          const itemEl = e.currentTarget.closest('.settings-item');
          const ci = parseInt(itemEl.dataset.ci, 10);
          const ii = parseInt(itemEl.dataset.ii, 10);
          const name = itemEl.querySelector('.si-name').value.trim();
          const url = itemEl.querySelector('.si-url').value.trim();
          const icon = itemEl.querySelector('.si-icon').value.trim();
          if (!name || !url || !url.includes('@@')) {
            alert('请填写名称与URL,且URL需要包含 @@ 作为关键词占位符');
            return;
          }
          this.navigationData[ci].list[ii] = { name, url, icon };
          this.saveNavigationData();
        });
      });

      page.querySelectorAll('.si-delete').forEach(btn => {
        btn.addEventListener('click', e => {
          const itemEl = e.currentTarget.closest('.settings-item');
          const ci = parseInt(itemEl.dataset.ci, 10);
          const ii = parseInt(itemEl.dataset.ii, 10);
          if (!confirm('确认删除该搜索引擎?')) return;
          this.navigationData[ci].list.splice(ii, 1);
          this.saveNavigationData();
          this.renderSettingsPage();
          this.attachSettingsHandlers();
        });
      });

      page.querySelectorAll('.cat-delete').forEach(btn => {
        btn.addEventListener('click', e => {
          const catEl = e.currentTarget.closest('.settings-cat');
          const ci = parseInt(catEl.dataset.ci, 10);
          const cat = this.navigationData[ci];
          const count = (cat && Array.isArray(cat.list)) ? cat.list.length : 0;
          if (!confirm(`确认删除分组「${cat?.name || ''}」?(其中包含 ${count} 个搜索引擎)`)) return;
          this.navigationData.splice(ci, 1);
          this.saveNavigationData();
          this.renderSettingsPage();
          this.attachSettingsHandlers();
        });
      });
    }

    askAi(trigger = 'manual') {
      if (this.isRequesting) {
        if (trigger === 'manual') {
          this.updateAiStatus('已有请求正在处理,请稍候...', 'warn');
        }
        return;
      }

      const query = this.getSearchKeyword();
      if (!query) {
        this.updateAiStatus('未检测到搜索关键词', 'warn');
        return;
      }

      if (!this.apiSettings.apiKey) {
        this.updateAiStatus('请先保存 SiliconFlow API Key', 'error');
        return;
      }

      if (trigger !== 'manual' && query === this.lastAnswerQuery) {
        return;
      }

      if (trigger === 'manual' && this.uiState.currentPage !== 'ai') {
        this.switchPage('ai');
      }

      const payload = {
        model: this.apiSettings.model,
        messages: [
          {
            role: 'system',
            content: '你是一名中文搜索助手,回答要简洁、可靠、引用常识。如缺少信息,请诚实说明。'
          },
          {
            role: 'user',
            content: `用户搜索词: ${query}
请给出一句话总结,并附上可能的参考链接(可选)。`
          }
        ],
        temperature: 0.3,
        max_tokens: 400,
        stream: false
      };

      this.isRequesting = true;
      this.updateAiStatus('正在向 SiliconFlow 请求回答...', 'info');
      if (this.aiElements?.answer) {
        this.aiElements.answer.textContent = '';
      }

      GM_xmlhttpRequest({
        method: 'POST',
        url: `${this.apiConfig.baseUrl}${this.apiConfig.chatPath}`,
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${this.apiSettings.apiKey}`
        },
        data: JSON.stringify(payload),
        timeout: 20000,
        onload: response => {
          this.isRequesting = false;
          if (response.status < 200 || response.status >= 300) {
            this.updateAiStatus(`请求失败,状态码 ${response.status}`, 'error');
            return;
          }
          let data;
          try {
            data = JSON.parse(response.responseText);
          } catch (err) {
            this.updateAiStatus('解析响应失败', 'error');
            return;
          }
          const errorMessage = data?.error?.message;
          if (errorMessage) {
            this.updateAiStatus(`服务返回错误: ${errorMessage}`, 'error');
            return;
          }
          const message = data?.choices?.[0]?.message?.content?.trim();
          if (!message) {
            this.updateAiStatus('AI 未返回内容', 'warn');
            return;
          }
          this.lastAnswerQuery = query;
          this.renderAiAnswer(message);
          this.updateAiStatus('AI 回答已生成', 'success');
        },
        onerror: () => {
          this.isRequesting = false;
          this.updateAiStatus('网络错误或被拦截', 'error');
        },
        ontimeout: () => {
          this.isRequesting = false;
          this.updateAiStatus('请求超时,请稍后重试', 'error');
        }
      });
    }

    renderAiAnswer(text) {
      if (!this.aiElements?.answer) return;
      this.aiElements.answer.textContent = text;
    }

    updateAiStatus(message, type = 'info') {
      if (!this.aiElements?.status) return;
      this.aiElements.status.textContent = message;
      this.aiElements.status.dataset.type = type;
    }

    persistApiKey(value) {
      if (!value) {
        GM_setValue('siliconflow_api_key', '');
        this.apiSettings.apiKey = '';
        this.uiState.showKeyEditor = true;
        this.updateAiStatus('API Key 已清除', 'warn');
        this.refreshKeyUi();
        return;
      }
      GM_setValue('siliconflow_api_key', value);
      this.apiSettings.apiKey = value;
      this.uiState.showKeyEditor = false;
      this.refreshKeyUi();
      this.updateAiStatus('API Key 已保存', 'success');
      if (this.aiElements?.keyInput) {
        this.aiElements.keyInput.value = '';
      }
    }

    persistModelValue(value) {
      if (!value || !this.apiConfig.models.some(item => item.value === value)) {
        this.apiSettings.model = this.apiConfig.defaultModel;
      } else {
        this.apiSettings.model = value;
      }
      GM_setValue('siliconflow_last_model', this.apiSettings.model);
      this.updateAiStatus(`已切换模型:${this.apiSettings.model}`, 'info');
      if (this.apiSettings.autoAsk) {
        this.askAi('auto');
      }
    }

    setAutoAsk(enabled) {
      this.apiSettings.autoAsk = Boolean(enabled);
      GM_setValue('siliconflow_auto_ask', this.apiSettings.autoAsk);
      if (this.aiElements?.autoToggle) {
        this.aiElements.autoToggle.checked = this.apiSettings.autoAsk;
      }
      if (this.apiSettings.autoAsk) {
        this.updateAiStatus('自动回答已开启', 'info');
        this.ensureAutoWatcher();
        this.askAi('auto');
      } else {
        this.clearAutoWatcher();
        this.updateAiStatus('自动回答已关闭', 'info');
      }
    }

    ensureAutoWatcher() {
      if (this.queryWatcher) return;
      this.queryWatcher = window.setInterval(() => {
        if (!this.apiSettings.autoAsk || this.isRequesting) return;
        const query = this.getSearchKeyword();
        if (!query) return;
        if (query === this.lastAnswerQuery) return;
        this.askAi('auto');
      }, 4000);
    }

    clearAutoWatcher() {
      if (this.queryWatcher) {
        window.clearInterval(this.queryWatcher);
        this.queryWatcher = null;
      }
    }

    refreshKeyUi() {
      if (!this.aiElements) return;
      const hasKey = Boolean(this.apiSettings.apiKey);
      const showEditor = this.uiState.showKeyEditor || !hasKey;
      const showStatus = hasKey && !showEditor;

      this.aiElements.keyRow?.classList.toggle('hidden', !showEditor);
      this.aiElements.keyStatus?.classList.toggle('hidden', !showStatus);

      if (this.aiElements.keyStatus) {
        this.aiElements.keyStatus.textContent = hasKey
          ? '密钥已安全保存,默认仅在本地可见。'
          : '粘贴后保存即可启用 AI 功能。';
      }

      if (this.aiElements.manageKeyBtn) {
        if (hasKey) {
          this.aiElements.manageKeyBtn.classList.remove('hidden');
          this.aiElements.manageKeyBtn.textContent = showEditor ? '取消编辑' : '重新配置密钥';
        } else {
          this.aiElements.manageKeyBtn.textContent = showEditor ? '隐藏输入' : '配置密钥';
          this.aiElements.manageKeyBtn.classList.toggle('hidden', showEditor);
        }
      }
    }

    waitForElement(selector, callback) {
      const interval = setInterval(() => {
        if (document.querySelector(selector)) {
          clearInterval(interval);
          callback();
        }
      }, 200);
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => new SearchEnhancer());
  } else {
    new SearchEnhancer();
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址