Greasy Fork

Greasy Fork is available in English.

豆瓣站外搜索下载

在豆瓣电影标题下添加多个站外搜索按钮(含源隐藏/恢复、手动排序、yellowrabbit可读页、KDocs自动搜索)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         豆瓣站外搜索下载
// @namespace    http://tampermonkey.net/
// @version      0.74
// @description  在豆瓣电影标题下添加多个站外搜索按钮(含源隐藏/恢复、手动排序、yellowrabbit可读页、KDocs自动搜索)
// @author       JIEMO
// @match        *://movie.douban.com/subject/*
// @match        *://www.kdocs.cn/*
// @match        *://appdocs.wpscdn.cn/*
// @match        *://lemonun.top/*
// @match        *://www.6v520.tv/*
// @icon         https://www.google.com/s2/favicons?domain=douban.com
// @grant        none
// @license      GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// ==/UserScript==

(function () {
  'use strict';

  const CONTAINER_CLASS = 'douban-jump-btn-container';
  const BTN_CLASS = 'douban-jump-btn';
  const STYLE_CLASS = 'douban-btn-style';
  const HIDDEN_SOURCES_KEY = 'douban_hidden_sources_v1';
  const SOURCE_ORDER_KEY = 'douban_source_order_v1';
  const SOURCE_LABEL_ALIASES_KEY = 'douban_source_label_aliases_v1';
  const SOURCE_MANAGER_MODAL_ID = 'douban-source-manager-modal';
  const KDOCS_OVERLAY_ID = 'douban-kdocs-overlay';
  const KDOCS_RUNNING_FLAG = '__douban_kdocs_search_running__';
  const KDOCS_MATCH_ATTR = 'data-douban-kdocs-match-id';
  const KDOCS_MATCH_HIGHLIGHT_CLASS = 'douban-kdocs-match-highlight';
  const KDOCS_SOURCES = [
    {
      id: 'orange-theater',
      label: '橙子剧场',
      url: 'https://www.kdocs.cn/l/cpCsvQoAunbY?R=L1MvMzU2',
      type: 'sheet',
    },
  ];

  const SOURCES = [
    {
      label: 'gying',
      buildUrl: (title) => `https://www.教父.com/s/1---1/${encodeURIComponent(title)}`,
    },
    {
      label: '不太灵',
      buildUrl: (title) => `https://web5.mukaku.com/search?sb=${encodeURIComponent(title)}`,
    },
    {
      label: '看片咖',
      buildUrl: (title) => `https://tv.kanpian.club/s/${encodeURIComponent(title)}.html`,
    },
    {
      label: 'yppan',
      buildUrl: (title) => `https://www.yppan.com/?s=${encodeURIComponent(title)}&cat=5`,
    },
    {
      label: '豆荚盘',
      buildUrl: (title) => `https://www.jpmom.com/?s=${encodeURIComponent(title)}`,
    },
    {
      label: '秒搜',
      buildUrl: (title) => `https://miaosou.fun/info?searchKey=${encodeURIComponent(title)}`,
    },
    {
      label: '1LOU',
      buildUrl: (title) => `https://www.1lou.me/search-${encodeAsUnderscoreUtf8Hex(title)}-1.htm`,
    },
    {
      label: 'btbtla',
      buildUrl: (title) => `https://www.btbtla.com/search/${title}`,
    },
    {
      label: '夸克猫',
      buildUrl: (title) => `https://www.kuakemao.com/?s=${encodeURIComponent(title)}`,
    },
    {
      label: '磁力柠檬',
      buildUrl: (title) => buildExternalAutoSearchUrl('https://lemonun.top/', 'lemonSearch', title),
      onClick: ({ title }) => openLemonSearch(title),
    },
    {
      label: 'BD影视',
      buildUrl: (title) => `https://www.bdjuhe.com/q/index----?k=${encodeURIComponent(title)}`,
    },
    {
      label: '爱恋动漫',
      buildUrl: (title) => `https://www.kisssub.org/search.php?keyword=${encodeURIComponent(title)}`,
    },
    {
      label: '七味',
      buildUrl: (title) => `https://www.qmp4.com/vs/-------------.html?wd=${encodeURIComponent(title)}`,
    },
    {
      label: 'SeedHub',
      buildUrl: (title) => `https://www.seedhub.cc/s/${encodeURIComponent(title)}`,
    },
    {
      label: '影巢',
      buildUrl: (title) => `https://hdhive.com/search?query=${encodeURIComponent(title)}&type=multi&page=1`,
    },
    {
      label: '盘尊社区',
      buildUrl: (title) => `https://www.panzun.cc/?q=${encodeURIComponent(title)}`,
    },
    {
      label: '海绵小站',
      buildUrl: (title) => `https://www.hmxz.org/search.htm?keyword=${encodeURIComponent(title)}`,
    },
    {
      label: '6v电影',
      buildUrl: (title) => buildExternalAutoSearchUrl('https://www.6v520.tv/sousuo.html', 'sixvSearch', title),
      onClick: ({ title }) => open6vMovieSearch(title),
    },
    ...KDOCS_SOURCES.map(function (source) {
      return {
        label: source.label,
        buildUrl: (title) => buildKdocsSearchUrl(source, title),
        onClick: ({ title }) => openKdocsViewer(source, title),
      };
    }),
    {
      label: 'yellowrabbit',
      buildUrl: (title) => `https://api.yellowrabbit.online/api/movie/search?key=${encodeURIComponent(title)}`,
      onClick: ({ title, url }) => openYellowrabbitViewer(title, url),
    },
  ];

  let renderTimer = 0;
  let sourceManagerLastFocused = null;

  function normalizeTitle(raw) {
    return String(raw || '')
      .replace(/\s+\(\d{4}\)\s*$/, '')
      .replace(/\s+/g, ' ')
      .trim();
  }

  function getMovieTitle() {
    const h1Element = document.querySelector('h1');
    if (!h1Element) return '';

    const itemReviewed = h1Element.querySelector('span[property="v:itemreviewed"]');
    const yearNode = h1Element.querySelector('.year');

    const itemReviewedText = normalizeTitle(itemReviewed ? itemReviewed.textContent : '');
    if (itemReviewedText) return itemReviewedText;

    const fullTitle = normalizeTitle(document.title.replace('(豆瓣)', ''));
    const yearText = normalizeTitle(yearNode ? yearNode.textContent : '').replace(/[()]/g, '');
    if (yearText) {
      return normalizeTitle(fullTitle.replace(new RegExp(`\\b${yearText}\\b`), ''));
    }

    return fullTitle;
  }

  function getSearchKeyword(title) {
    const normalized = normalizeTitle(title);
    if (!normalized) return '';
    return normalized.split(' ')[0].trim();
  }

  function getDefaultSourceLabels() {
    return SOURCES.map(function (source) {
      return source.label;
    });
  }

  function loadSourceOrder() {
    const defaultLabels = getDefaultSourceLabels();
    const allLabels = new Set(defaultLabels);

    try {
      const raw = localStorage.getItem(SOURCE_ORDER_KEY);
      if (!raw) return defaultLabels.slice();
      const parsed = JSON.parse(raw);
      if (!Array.isArray(parsed)) return defaultLabels.slice();

      const seen = new Set();
      const ordered = parsed.filter(function (label) {
        if (typeof label !== 'string' || !allLabels.has(label) || seen.has(label)) {
          return false;
        }
        seen.add(label);
        return true;
      });
      const orderedSet = new Set(ordered);

      for (const label of defaultLabels) {
        if (!orderedSet.has(label)) {
          ordered.push(label);
        }
      }

      return ordered;
    } catch (_error) {
      return defaultLabels.slice();
    }
  }

  function saveSourceOrder(labels) {
    localStorage.setItem(SOURCE_ORDER_KEY, JSON.stringify(labels));
  }

  function clearSourceOrder() {
    localStorage.removeItem(SOURCE_ORDER_KEY);
  }

  function normalizeSourceDisplayLabel(value, fallbackLabel) {
    const normalized = String(value == null ? '' : value).trim();
    return normalized || fallbackLabel;
  }

  function loadSourceLabelAliases() {
    const allLabels = new Set(getDefaultSourceLabels());

    try {
      const raw = localStorage.getItem(SOURCE_LABEL_ALIASES_KEY);
      if (!raw) return {};
      const parsed = JSON.parse(raw);
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};

      return Object.keys(parsed).reduce(function (aliases, label) {
        const value = parsed[label];
        if (!allLabels.has(label) || typeof value !== 'string') {
          return aliases;
        }

        const displayLabel = normalizeSourceDisplayLabel(value, label);
        if (displayLabel !== label) {
          aliases[label] = displayLabel;
        }
        return aliases;
      }, {});
    } catch (_error) {
      return {};
    }
  }

  function saveSourceLabelAliases(labelAliases) {
    localStorage.setItem(SOURCE_LABEL_ALIASES_KEY, JSON.stringify(labelAliases));
  }

  function clearSourceLabelAliases() {
    localStorage.removeItem(SOURCE_LABEL_ALIASES_KEY);
  }

  function getSourceDisplayLabel(label, labelAliases) {
    if (!labelAliases || typeof labelAliases[label] !== 'string') {
      return label;
    }
    return normalizeSourceDisplayLabel(labelAliases[label], label);
  }

  function getOrderedSources() {
    const sourceMap = new Map(SOURCES.map(function (source) {
      return [source.label, source];
    }));

    return loadSourceOrder().map(function (label) {
      return sourceMap.get(label);
    }).filter(Boolean);
  }

  function getAllSourceLabels() {
    return getOrderedSources().map(function (source) {
      return source.label;
    });
  }

  function loadHiddenSources() {
    const allLabels = new Set(getDefaultSourceLabels());

    try {
      const raw = localStorage.getItem(HIDDEN_SOURCES_KEY);
      if (!raw) return new Set();
      const parsed = JSON.parse(raw);
      if (!Array.isArray(parsed)) return new Set();

      return new Set(parsed.filter(function (label) {
        return typeof label === 'string' && allLabels.has(label);
      }));
    } catch (_error) {
      return new Set();
    }
  }

  function saveHiddenSources(hiddenSet) {
    localStorage.setItem(HIDDEN_SOURCES_KEY, JSON.stringify(Array.from(hiddenSet)));
  }

  function hideSource(label) {
    const hidden = loadHiddenSources();
    hidden.add(label);
    saveHiddenSources(hidden);
  }

  function toggleSourceHidden(label) {
    const hidden = loadHiddenSources();
    if (hidden.has(label)) {
      hidden.delete(label);
      saveHiddenSources(hidden);
      return false;
    }

    hidden.add(label);
    saveHiddenSources(hidden);
    return true;
  }

  function clearHiddenSources() {
    localStorage.removeItem(HIDDEN_SOURCES_KEY);
  }

  function openSourceOrderManager() {
    openSourceManager();
  }

  function openSourceManager() {
    if (!document.body) return;

    sourceManagerLastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
    closeSourceManager();

    const defaultLabels = getDefaultSourceLabels();
    const state = {
      labels: getAllSourceLabels(),
      hidden: loadHiddenSources(),
      labelAliases: loadSourceLabelAliases(),
      draggedLabel: '',
    };

    const mask = document.createElement('div');
    mask.id = SOURCE_MANAGER_MODAL_ID;
    mask.className = 'douban-source-manager-mask';

    const panel = document.createElement('section');
    panel.className = 'douban-source-manager-panel';
    panel.tabIndex = -1;
    panel.setAttribute('role', 'dialog');
    panel.setAttribute('aria-modal', 'true');
    panel.setAttribute('aria-label', '源设置');

    const header = document.createElement('div');
    header.className = 'douban-source-manager-header';

    const titleWrap = document.createElement('div');
    titleWrap.className = 'douban-source-manager-title-wrap';

    const title = document.createElement('h2');
    title.className = 'douban-source-manager-title';
    title.textContent = '源设置';

    const subtitle = document.createElement('div');
    subtitle.className = 'douban-source-manager-subtitle';
    subtitle.textContent = '点击卡片按钮切换显示,拖动卡片调整顺序,也可直接修改显示名称。';

    titleWrap.appendChild(title);
    titleWrap.appendChild(subtitle);

    const closeBtn = document.createElement('button');
    closeBtn.type = 'button';
    closeBtn.className = 'douban-source-manager-close';
    closeBtn.setAttribute('aria-label', '关闭源设置');
    closeBtn.textContent = '×';

    header.appendChild(titleWrap);
    header.appendChild(closeBtn);

    const toolbar = document.createElement('div');
    toolbar.className = 'douban-source-manager-toolbar';

    const resetOrderBtn = document.createElement('button');
    resetOrderBtn.type = 'button';
    resetOrderBtn.className = 'douban-source-manager-action';
    resetOrderBtn.textContent = '恢复默认排序';

    const resetVisibleBtn = document.createElement('button');
    resetVisibleBtn.type = 'button';
    resetVisibleBtn.className = 'douban-source-manager-action';
    resetVisibleBtn.textContent = '恢复全部显示';

    toolbar.appendChild(resetOrderBtn);
    toolbar.appendChild(resetVisibleBtn);

    const summary = document.createElement('div');
    summary.className = 'douban-source-manager-summary';

    const list = document.createElement('ul');
    list.className = 'douban-source-manager-list';

    const footer = document.createElement('div');
    footer.className = 'douban-source-manager-footer';

    const cancelBtn = document.createElement('button');
    cancelBtn.type = 'button';
    cancelBtn.className = 'douban-source-manager-btn-secondary';
    cancelBtn.textContent = '取消';

    const saveBtn = document.createElement('button');
    saveBtn.type = 'button';
    saveBtn.className = 'douban-source-manager-btn-primary';
    saveBtn.textContent = '保存';

    footer.appendChild(cancelBtn);
    footer.appendChild(saveBtn);

    panel.appendChild(header);
    panel.appendChild(toolbar);
    panel.appendChild(summary);
    panel.appendChild(list);
    panel.appendChild(footer);
    mask.appendChild(panel);

    function renderSummary() {
      const hiddenCount = state.hidden.size;
      const visibleCount = state.labels.length - hiddenCount;
      summary.textContent = '共 ' + state.labels.length + ' 个源,当前显示 ' + visibleCount + ' 个,隐藏 ' + hiddenCount + ' 个。';
    }

    function moveLabelToIndex(draggedLabel, targetIndex) {
      if (!draggedLabel) return;
      const nextLabels = state.labels.slice();
      const sourceIndex = nextLabels.indexOf(draggedLabel);
      if (sourceIndex === -1) return;

      nextLabels.splice(sourceIndex, 1);
      const safeIndex = Math.max(0, Math.min(targetIndex, nextLabels.length));
      nextLabels.splice(safeIndex, 0, draggedLabel);
      state.labels = nextLabels;
    }

    function getReorderEntries() {
      return Array.from(list.querySelectorAll('.douban-source-manager-item')).filter(function (item) {
        return item.getAttribute('data-label') !== state.draggedLabel;
      }).map(function (item, index) {
        return {
          item: item,
          index: index,
          rect: item.getBoundingClientRect(),
        };
      });
    }

    function clearDropIndicators() {
      Array.from(list.querySelectorAll('.is-drop-target, .is-drop-after')).forEach(function (node) {
        node.classList.remove('is-drop-target', 'is-drop-after');
      });
    }

    function updateDropIndicators(insertIndex) {
      clearDropIndicators();

      const entries = getReorderEntries();
      if (entries.length === 0) return;

      if (insertIndex >= entries.length) {
        entries[entries.length - 1].item.classList.add('is-drop-after');
        return;
      }

      entries[insertIndex].item.classList.add('is-drop-target');
    }

    function getInsertIndexFromPointer(clientX, clientY) {
      const entries = getReorderEntries();
      if (entries.length === 0) return 0;

      const rows = [];
      entries.forEach(function (entry) {
        const lastRow = rows[rows.length - 1];
        const sameRow = lastRow && Math.abs(lastRow.top - entry.rect.top) < Math.max(12, entry.rect.height / 2);
        if (sameRow) {
          lastRow.entries.push(entry);
          lastRow.bottom = Math.max(lastRow.bottom, entry.rect.bottom);
          return;
        }

        rows.push({
          top: entry.rect.top,
          bottom: entry.rect.bottom,
          entries: [entry],
        });
      });

      for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
        const row = rows[rowIndex];
        const rowMiddle = row.top + (row.bottom - row.top) / 2;
        if (clientY <= rowMiddle) {
          for (let entryIndex = 0; entryIndex < row.entries.length; entryIndex += 1) {
            const entry = row.entries[entryIndex];
            const entryMiddle = entry.rect.left + entry.rect.width / 2;
            if (clientX <= entryMiddle) {
              return entry.index;
            }
          }

          return row.entries[row.entries.length - 1].index + 1;
        }
      }

      return entries.length;
    }

    function handleListDragOver(event) {
      event.preventDefault();
      if (event.dataTransfer) {
        event.dataTransfer.dropEffect = 'move';
      }
      if (!state.draggedLabel) return;

      updateDropIndicators(getInsertIndexFromPointer(event.clientX, event.clientY));
    }

    function handleListDrop(event) {
      event.preventDefault();
      const draggedLabel = state.draggedLabel || (event.dataTransfer ? event.dataTransfer.getData('text/plain') : '');
      if (!draggedLabel) return;

      moveLabelToIndex(draggedLabel, getInsertIndexFromPointer(event.clientX, event.clientY));
      clearDropIndicators();
      renderList();
    }

    function renderList() {
      list.innerHTML = '';
      renderSummary();

      state.labels.forEach(function (label, index) {
        const item = document.createElement('li');
        const isHidden = state.hidden.has(label);
        const displayLabel = getSourceDisplayLabel(label, state.labelAliases);

        item.className = 'douban-source-manager-item' + (isHidden ? ' is-hidden' : '');
        item.draggable = true;
        item.setAttribute('data-label', label);

        const indexNode = document.createElement('span');
        indexNode.className = 'douban-source-manager-index';
        indexNode.textContent = String(index + 1).padStart(2, '0');

        const main = document.createElement('div');
        main.className = 'douban-source-manager-main';

        const nameGroup = document.createElement('div');
        nameGroup.className = 'douban-source-manager-name-group';

        const itemTop = document.createElement('div');
        itemTop.className = 'douban-source-manager-item-top';

        const labelNode = document.createElement('div');
        labelNode.className = 'douban-source-manager-label';
        labelNode.textContent = displayLabel;

        const metaNode = document.createElement('div');
        metaNode.className = 'douban-source-manager-meta';
        metaNode.textContent = '原始名称:' + label;

        const renameInput = document.createElement('input');
        renameInput.type = 'text';
        renameInput.className = 'douban-source-manager-rename';
        renameInput.value = displayLabel;
        renameInput.placeholder = label;
        renameInput.draggable = false;
        renameInput.setAttribute('aria-label', '修改源“' + label + '”的显示名称');

        renameInput.addEventListener('mousedown', function (event) {
          event.stopPropagation();
        });

        renameInput.addEventListener('dragstart', function (event) {
          event.preventDefault();
          event.stopPropagation();
        });

        renameInput.addEventListener('input', function () {
          const nextDisplayLabel = normalizeSourceDisplayLabel(renameInput.value, label);
          labelNode.textContent = nextDisplayLabel;

          if (nextDisplayLabel === label) {
            delete state.labelAliases[label];
          } else {
            state.labelAliases[label] = nextDisplayLabel;
          }
        });

        renameInput.addEventListener('blur', function () {
          renameInput.value = getSourceDisplayLabel(label, state.labelAliases);
        });

        const toggleBtn = document.createElement('button');
        toggleBtn.type = 'button';
        toggleBtn.className = 'douban-source-manager-toggle' + (isHidden ? ' is-hidden' : '');
        toggleBtn.textContent = isHidden ? '显示' : '隐藏';

        toggleBtn.addEventListener('click', function () {
          if (state.hidden.has(label)) {
            state.hidden.delete(label);
          } else {
            state.hidden.add(label);
          }
          renderList();
        });

        item.addEventListener('dragstart', function (event) {
          state.draggedLabel = label;
          if (event.dataTransfer) {
            event.dataTransfer.effectAllowed = 'move';
            event.dataTransfer.setData('text/plain', label);
          }
          item.classList.add('is-dragging');
        });

        item.addEventListener('dragover', function (event) {
          handleListDragOver(event);
        });

        item.addEventListener('dragleave', function () {
          item.classList.remove('is-drop-target', 'is-drop-after');
        });

        item.addEventListener('drop', function (event) {
          event.stopPropagation();
          handleListDrop(event);
        });

        item.addEventListener('dragend', function () {
          state.draggedLabel = '';
          Array.from(list.querySelectorAll('.is-drop-target, .is-drop-after, .is-dragging')).forEach(function (node) {
            node.classList.remove('is-drop-target', 'is-drop-after', 'is-dragging');
          });
        });

        itemTop.appendChild(indexNode);
        itemTop.appendChild(toggleBtn);

        main.appendChild(itemTop);
        nameGroup.appendChild(labelNode);
        nameGroup.appendChild(metaNode);
        nameGroup.appendChild(renameInput);
        main.appendChild(nameGroup);
        item.appendChild(main);
        list.appendChild(item);
      });
    }

    resetOrderBtn.addEventListener('click', function () {
      state.labels = defaultLabels.slice();
      renderList();
    });

    resetVisibleBtn.addEventListener('click', function () {
      state.hidden.clear();
      renderList();
    });

    list.addEventListener('dragover', function (event) {
      handleListDragOver(event);
    });

    list.addEventListener('drop', function (event) {
      handleListDrop(event);
    });

    closeBtn.addEventListener('click', closeSourceManager);
    cancelBtn.addEventListener('click', closeSourceManager);

    saveBtn.addEventListener('click', function () {
      if (state.hidden.size > 0) {
        saveHiddenSources(state.hidden);
      } else {
        clearHiddenSources();
      }

      if (JSON.stringify(state.labels) === JSON.stringify(defaultLabels)) {
        clearSourceOrder();
      } else {
        saveSourceOrder(state.labels.slice());
      }

      if (Object.keys(state.labelAliases).length > 0) {
        saveSourceLabelAliases(state.labelAliases);
      } else {
        clearSourceLabelAliases();
      }

      closeSourceManager();
      renderButtons(true);
    });

    mask.addEventListener('click', function (event) {
      if (event.target === mask) {
        closeSourceManager();
      }
    });

    document.body.appendChild(mask);
    document.addEventListener('keydown', handleSourceManagerKeydown, true);
    renderList();
    closeBtn.focus();
  }

  function closeSourceManager() {
    const existing = document.getElementById(SOURCE_MANAGER_MODAL_ID);
    if (existing) {
      existing.remove();
    }
    document.removeEventListener('keydown', handleSourceManagerKeydown, true);
    if (sourceManagerLastFocused && document.contains(sourceManagerLastFocused)) {
      sourceManagerLastFocused.focus();
    }
    sourceManagerLastFocused = null;
  }

  function handleSourceManagerKeydown(event) {
    const modal = document.getElementById(SOURCE_MANAGER_MODAL_ID);
    if (!modal) return;

    if (event.key === 'Escape') {
      event.preventDefault();
      closeSourceManager();
      return;
    }

    if (event.key !== 'Tab') return;

    const focusables = Array.from(modal.querySelectorAll('button, input, [href], [tabindex]:not([tabindex="-1"])')).filter(function (node) {
      return node instanceof HTMLElement && !node.hasAttribute('disabled') && node.tabIndex !== -1;
    });

    if (focusables.length === 0) return;

    const first = focusables[0];
    const last = focusables[focusables.length - 1];
    const active = document.activeElement;

    if (event.shiftKey && active === first) {
      event.preventDefault();
      last.focus();
      return;
    }

    if (!event.shiftKey && active === last) {
      event.preventDefault();
      first.focus();
    }
  }

  function encodeAsUnderscoreUtf8Hex(text) {
    const bytes = new TextEncoder().encode(String(text || ''));
    let output = '';
    for (const value of bytes) {
      output += `_${value.toString(16).toUpperCase().padStart(2, '0')}`;
    }
    return output;
  }

  function getKdocsSourceById(sourceId) {
    return KDOCS_SOURCES.find(function (source) {
      return source.id === sourceId;
    }) || null;
  }

  function buildKdocsSearchUrl(source, keyword) {
    const config = {
      doubanMode: 'kdocsSearch',
      sourceId: source.id,
      sourceType: source.type,
      keyword: keyword,
      sheetLabel: source.label,
    };
    const url = new URL(source.url);
    url.searchParams.set('doubanMode', config.doubanMode);
    url.searchParams.set('doubanSourceId', config.sourceId);
    url.searchParams.set('doubanSourceType', config.sourceType);
    url.searchParams.set('doubanKeyword', config.keyword);
    url.searchParams.set('doubanSheetLabel', config.sheetLabel);
    url.hash = new URLSearchParams(config).toString();

    return url.toString();
  }

  function openKdocsViewer(source, keyword) {
    const url = buildKdocsSearchUrl(source, keyword);
    window.open(url, '_blank');
  }

  function parseKdocsSearchConfig() {
    const hash = String(window.location.hash || '');
    const hashParams = hash.startsWith('#') ? new URLSearchParams(hash.slice(1)) : null;
    const searchParams = new URLSearchParams(window.location.search || '');
    const mode = (hashParams && hashParams.get('doubanMode')) || searchParams.get('doubanMode') || '';
    if (mode !== 'kdocsSearch') return null;

    const sourceId = (hashParams && hashParams.get('sourceId')) || searchParams.get('doubanSourceId') || '';
    const sourceTypeFromQuery = (hashParams && hashParams.get('sourceType')) || searchParams.get('doubanSourceType') || '';
    const matchedSource = getKdocsSourceById(sourceId);
    const keyword = (hashParams && hashParams.get('keyword')) || searchParams.get('doubanKeyword') || '';
    const sheetLabel = (hashParams && hashParams.get('sheetLabel'))
      || searchParams.get('doubanSheetLabel')
      || (matchedSource && matchedSource.label)
      || document.title
      || 'KDocs';
    const sourceType = sourceTypeFromQuery || (matchedSource && matchedSource.type) || 'sheet';
    if (!keyword) return null;

    return { keyword, sheetLabel, sourceId, sourceType };
  }

  function ensureKdocsOverlayStyle() {
    if (document.getElementById(KDOCS_OVERLAY_ID + '-style')) return;

    const style = document.createElement('style');
    style.id = KDOCS_OVERLAY_ID + '-style';
    style.textContent = `
      #${KDOCS_OVERLAY_ID} {
        position: fixed;
        top: 16px;
        right: 16px;
        width: min(440px, calc(100vw - 24px));
        max-height: calc(100vh - 32px);
        overflow: auto;
        z-index: 2147483647;
        background: rgba(255, 255, 255, 0.98);
        border: 1px solid #cfe0f6;
        border-radius: 14px;
        box-shadow: 0 14px 38px rgba(15, 23, 42, 0.18);
        color: #0f172a;
        font: 14px/1.55 -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif;
      }

      #${KDOCS_OVERLAY_ID} * {
        box-sizing: border-box;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-head {
        padding: 12px 14px 8px;
        border-bottom: 1px solid #e3ecf8;
        background: linear-gradient(180deg, #f7fbff 0%, #ffffff 100%);
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-title {
        margin: 0;
        font-size: 17px;
        font-weight: 700;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-sub,
      #${KDOCS_OVERLAY_ID} .douban-kdocs-count,
      #${KDOCS_OVERLAY_ID} .douban-kdocs-empty {
        margin-top: 4px;
        color: #4b5563;
        word-break: break-word;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-status {
        margin-top: 6px;
        font-weight: 600;
        color: #1565c0;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-status.is-error {
        color: #b91c1c;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-actions {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        margin-top: 10px;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-btn {
        appearance: none;
        border: 1px solid #c8d7ef;
        background: #eef5ff;
        color: #1565c0;
        border-radius: 999px;
        padding: 6px 12px;
        font: inherit;
        font-weight: 600;
        cursor: pointer;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-body {
        padding: 10px 14px 14px;
        display: grid;
        gap: 8px;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-row {
        border: 1px solid #dbe6f4;
        background: #f8fbff;
        border-radius: 10px;
        padding: 8px 10px;
        word-break: break-word;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-row[data-anchor-id] {
        cursor: pointer;
        transition: background 0.2s ease, border-color 0.2s ease;
      }

      #${KDOCS_OVERLAY_ID} .douban-kdocs-row[data-anchor-id]:hover {
        background: #eef5ff;
        border-color: #b7ccef;
      }

      [${KDOCS_MATCH_ATTR}].${KDOCS_MATCH_HIGHLIGHT_CLASS} {
        outline: 3px solid rgba(21, 101, 192, 0.35);
        background: rgba(255, 243, 205, 0.75) !important;
        border-radius: 8px;
        transition: outline 0.2s ease, background 0.2s ease;
      }

      @media (max-width: 720px) {
        #${KDOCS_OVERLAY_ID} {
          top: auto;
          right: 8px;
          bottom: 8px;
          left: 8px;
          width: auto;
          max-height: 55vh;
        }
      }
    `;

    document.head.appendChild(style);
  }

  function escapeHtml(input) {
    return String(input || '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  function kdocsNormalizeOverlayItems(result) {
    if (Array.isArray(result.items) && result.items.length > 0) {
      return result.items.map(function (item) {
        if (item && typeof item === 'object') {
          return {
            text: String(item.text || ''),
            anchorId: item.anchorId ? String(item.anchorId) : '',
          };
        }
        return { text: String(item || ''), anchorId: '' };
      }).filter(function (item) {
        return Boolean(item.text);
      });
    }

    return (Array.isArray(result.rows) ? result.rows : []).map(function (row) {
      return { text: String(row || ''), anchorId: '' };
    }).filter(function (item) {
      return Boolean(item.text);
    });
  }

  function kdocsFindMatchNode(anchorId) {
    if (!anchorId) return null;
    return Array.from(document.querySelectorAll('[' + KDOCS_MATCH_ATTR + ']')).find(function (node) {
      return node.getAttribute(KDOCS_MATCH_ATTR) === anchorId;
    }) || null;
  }

  function kdocsScrollToMatch(anchorId) {
    const node = kdocsFindMatchNode(anchorId);
    if (!node) return false;

    node.scrollIntoView({ behavior: 'smooth', block: 'center' });
    node.classList.add(KDOCS_MATCH_HIGHLIGHT_CLASS);
    window.setTimeout(function () {
      node.classList.remove(KDOCS_MATCH_HIGHLIGHT_CLASS);
    }, 2200);
    return true;
  }

  function renderKdocsSearchOverlay(config, result) {
    ensureKdocsOverlayStyle();

    let root = document.getElementById(KDOCS_OVERLAY_ID);
    if (!root) {
      root = document.createElement('section');
      root.id = KDOCS_OVERLAY_ID;
      document.body.appendChild(root);
    }

    const items = kdocsNormalizeOverlayItems(result);
    const countText = result.countText || (items.length > 0 ? ('结果数: ' + items.length) : '');
    const statusClass = result.errorText ? 'douban-kdocs-status is-error' : 'douban-kdocs-status';
    const statusText = result.statusText || (result.errorText ? ('搜索失败: ' + result.errorText) : '搜索完成');
    const rowsHtml = items.length > 0
      ? items.map(function (item, index) {
          const anchorAttr = item.anchorId ? ' data-anchor-id="' + escapeHtml(item.anchorId) + '"' : '';
          return '<div class="douban-kdocs-row" data-item-index="' + String(index) + '"' + anchorAttr + '>' + escapeHtml(item.text) + '</div>';
        }).join('')
      : '<div class="douban-kdocs-empty">未找到匹配行</div>';

    root.innerHTML = ''
      + '<div class="douban-kdocs-head">'
      +   '<h1 class="douban-kdocs-title">' + escapeHtml(config.sheetLabel || 'KDocs') + ' 自动搜索</h1>'
      +   '<div class="douban-kdocs-sub">关键词: ' + escapeHtml(config.keyword) + '</div>'
      +   '<div class="' + statusClass + '">' + escapeHtml(statusText) + '</div>'
      +   (countText ? '<div class="douban-kdocs-count">' + escapeHtml(countText) + '</div>' : '')
      +   '<div class="douban-kdocs-actions">'
      +     '<button type="button" class="douban-kdocs-btn" data-action="retry">重新搜索</button>'
      +     '<button type="button" class="douban-kdocs-btn" data-action="close">关闭结果</button>'
      +   '</div>'
      + '</div>'
      + '<div class="douban-kdocs-body">' + rowsHtml + '</div>';

    const retryBtn = root.querySelector('[data-action="retry"]');
    const closeBtn = root.querySelector('[data-action="close"]');

    if (retryBtn) {
      retryBtn.onclick = function () {
        runKdocsAutoSearch(config);
      };
    }

    if (closeBtn) {
      closeBtn.onclick = function () {
        root.remove();
      };
    }

    Array.from(root.querySelectorAll('.douban-kdocs-row[data-anchor-id]')).forEach(function (rowNode) {
      rowNode.onclick = function () {
        const anchorId = rowNode.getAttribute('data-anchor-id') || '';
        kdocsScrollToMatch(anchorId);
      };
    });

    const firstAnchorItem = items.find(function (item) {
      return Boolean(item.anchorId);
    });
    if (firstAnchorItem && !result.errorText) {
      window.setTimeout(function () {
        kdocsScrollToMatch(firstAnchorItem.anchorId);
      }, 120);
    }
  }

  function kdocsSleep(ms) {
    return new Promise(function (resolve) {
      window.setTimeout(resolve, ms);
    });
  }

  function kdocsTextOf(node) {
    return String((node && (node.innerText || node.textContent)) || '').trim();
  }

  function kdocsFindByText(selector, text, exact) {
    return Array.from(document.querySelectorAll(selector)).find(function (node) {
      const value = kdocsTextOf(node);
      return exact ? value === text : value.indexOf(text) !== -1;
    }) || null;
  }

  function kdocsClick(node) {
    if (!node) return false;
    node.click();
    return true;
  }

  function kdocsSetInputValue(input, value) {
    if (!input) return false;

    const prototype = input.tagName === 'TEXTAREA'
      ? window.HTMLTextAreaElement.prototype
      : window.HTMLInputElement.prototype;
    const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');

    if (descriptor && typeof descriptor.set === 'function') {
      descriptor.set.call(input, value);
    } else {
      input.value = value;
    }

    input.dispatchEvent(new Event('input', { bubbles: true }));
    input.dispatchEvent(new Event('change', { bubbles: true }));
    return true;
  }

  function kdocsCloseLoginModal() {
    const closeBtn = document.querySelector('.component-login-modal-pop .kdv-button');
    if (closeBtn) {
      closeBtn.click();
    }
  }

  function kdocsNormalizeResultText(text) {
    return String(text || '').replace(/\s+/g, ' ').trim();
  }

  function kdocsCollectTexts(selectors) {
    const values = [];
    for (const selector of selectors) {
      const nodes = document.querySelectorAll(selector);
      for (const node of nodes) {
        const text = kdocsNormalizeResultText(kdocsTextOf(node));
        if (!text) continue;
        values.push(text);
      }
      if (values.length > 0) {
        break;
      }
    }
    return values;
  }

  function kdocsSplitTextLines(text) {
    return String(text || '')
      .split(/\r?\n+/)
      .map(function (line) {
        return kdocsNormalizeResultText(line);
      })
      .filter(Boolean);
  }

  function kdocsIncludesKeyword(text, keyword) {
    return String(text || '').toLowerCase().indexOf(String(keyword || '').toLowerCase()) !== -1;
  }

  async function kdocsResolveValue(value) {
    if (value && typeof value.then === 'function') {
      return await value;
    }
    return value;
  }

  function kdocsIsNoiseLine(text, keyword) {
    if (!text) return true;
    if (text === keyword) return true;
    if (text.indexOf('重新搜索') !== -1) return true;
    if (text.indexOf('关闭结果') !== -1) return true;
    if (text.indexOf('正在自动搜索') !== -1) return true;
    if (text.indexOf('自动搜索超时') !== -1) return true;
    if (text.indexOf('查找全部') !== -1) return true;
    return false;
  }

  function kdocsFilterKeywordLines(lines, keyword) {
    return lines.filter(function (line) {
      if (!kdocsIncludesKeyword(line, keyword)) return false;
      if (line.length > 500) return false;
      return !kdocsIsNoiseLine(line, keyword);
    });
  }

  function kdocsFilterKeywordItems(items, keyword) {
    return items.filter(function (item) {
      const text = item && typeof item === 'object' ? String(item.text || '') : String(item || '');
      if (!kdocsIncludesKeyword(text, keyword)) return false;
      if (text.length > 500) return false;
      return !kdocsIsNoiseLine(text, keyword);
    }).map(function (item) {
      if (item && typeof item === 'object') return item;
      return { text: String(item || ''), anchorId: '' };
    });
  }

  function kdocsCreateAnchorId() {
    return 'kdocs-match-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
  }

  function kdocsEnsureMatchAnchor(node) {
    if (!node || !node.setAttribute) return '';
    let anchorId = node.getAttribute(KDOCS_MATCH_ATTR) || '';
    if (!anchorId) {
      anchorId = kdocsCreateAnchorId();
      node.setAttribute(KDOCS_MATCH_ATTR, anchorId);
    }
    return anchorId;
  }

  function kdocsUniqueItems(items) {
    const seen = new Set();
    return items.filter(function (item) {
      const key = (item.anchorId || '') + '::' + item.text;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function kdocsIsVisibleElement(node) {
    if (!node || node.nodeType !== 1) return false;
    const style = window.getComputedStyle(node);
    if (!style) return true;
    if (style.display === 'none' || style.visibility === 'hidden') return false;
    return true;
  }

  function kdocsIsClickableElement(node) {
    if (!node || node.nodeType !== 1) return false;
    const tagName = String(node.tagName || '').toLowerCase();
    if (['button', 'a', 'summary'].indexOf(tagName) !== -1) return true;
    if (node.hasAttribute('role')) {
      const role = String(node.getAttribute('role') || '').toLowerCase();
      if (['tab', 'button', 'link', 'menuitem', 'option', 'switch', 'radio'].indexOf(role) !== -1) return true;
    }
    if (node.hasAttribute('tabindex')) return true;
    if (node.hasAttribute('aria-selected')) return true;
    if (node.hasAttribute('aria-pressed')) return true;
    if (node.hasAttribute('aria-expanded')) return true;
    if (node.hasAttribute('aria-checked')) return true;
    if (typeof node.onclick === 'function') return true;
    const style = window.getComputedStyle(node);
    return Boolean(style && style.cursor === 'pointer');
  }

  function kdocsNormalizeSectionLabel(text, node) {
    const normalized = kdocsNormalizeResultText(text);
    if (!normalized || normalized.length > 40) return '';
    const hasTabSemantics = Boolean(
      node
      && (
        node.getAttribute('role') === 'tab'
        || node.hasAttribute('aria-selected')
        || node.hasAttribute('aria-controls')
        || node.closest('[role="tablist"]')
        || node.closest('[class*="tab"]')
        || node.closest('[class*="segmented"]')
        || node.closest('[class*="segment"]')
      )
    );
    if (/^[0-9]+$/.test(normalized) && !hasTabSemantics) return '';
    if (normalized.length < 2 && !hasTabSemantics) return '';
    return normalized;
  }

  function kdocsGetDocSectionSwitches() {
    const selector = [
      '[role="tablist"] [role="tab"]',
      '[role="tablist"] [aria-selected]',
      '[role="tab"]',
      '[aria-selected]',
      '[aria-controls]',
      '[aria-owns]',
      '[class*="tab"]',
      '[class*="segmented"] [role="button"]',
      '[class*="segment"] [role="button"]',
      '[class*="toggle"] [role="button"]',
      '[class*="catalog"] [class*="item"]',
      '[class*="toc"] [class*="item"]',
      '[class*="nav"] [class*="item"]',
      '[class*="menu"] [class*="item"]',
      '[class*="outline"] [class*="item"]',
      '[class*="sidebar"] [class*="item"]',
      '[class*="switch"] [class*="item"]',
      '[data-tab]',
      '[data-panel]',
      '[data-column]',
      '[data-col]',
      '[data-index]',
      'nav [role="tab"]',
      'nav button',
      'nav a',
      '[class*="tabs"] *',
    ].join(', ');

    const seen = new Set();
    const results = [];
    Array.from(document.querySelectorAll(selector)).forEach(function (node) {
      if (!kdocsIsVisibleElement(node)) return;
      if (node.closest('#' + KDOCS_OVERLAY_ID)) return;
      const clickable = kdocsIsClickableElement(node) ? node : node.closest('button, a, [role="tab"], [role="button"], [tabindex]');
      if (!clickable || !kdocsIsVisibleElement(clickable)) return;
      const label = kdocsNormalizeSectionLabel(kdocsTextOf(clickable), clickable);
      if (!label) return;
      const key = (clickable.getAttribute('aria-controls') || clickable.getAttribute('data-tab') || clickable.getAttribute('data-column') || label)
        + '::' + String(clickable.className || '');
      if (seen.has(key)) return;
      seen.add(key);
      results.push({ node: clickable, label: label });
    });

    return results.slice(0, 20);
  }

  function kdocsFindGenericDocMatchItems(config) {
    if (!document.body) return [];

    const results = [];
    const nodes = document.body.querySelectorAll('*');
    for (const node of nodes) {
      if (!kdocsIsVisibleElement(node)) continue;
      if (node.closest('#' + KDOCS_OVERLAY_ID)) continue;
      const tagName = String(node.tagName || '').toLowerCase();
      if (['script', 'style', 'noscript', 'svg', 'path'].indexOf(tagName) !== -1) continue;

      const text = kdocsNormalizeResultText(kdocsTextOf(node));
      if (!text || text.length < config.keyword.length || text.length > 500) continue;
      if (!kdocsIncludesKeyword(text, config.keyword)) continue;
      if (kdocsIsNoiseLine(text, config.keyword)) continue;

      const childContains = Array.from(node.children || []).some(function (child) {
        if (!kdocsIsVisibleElement(child)) return false;
        const childText = kdocsNormalizeResultText(kdocsTextOf(child));
        return childText && childText.length <= 500 && kdocsIncludesKeyword(childText, config.keyword);
      });
      if (childContains) continue;

      results.push({ text: text, anchorId: kdocsEnsureMatchAnchor(node) });
      if (results.length >= 20) break;
    }

    return results;
  }

  async function kdocsScanDocSectionsForMatches(config) {
    const sections = kdocsGetDocSectionSwitches();
    for (const section of sections) {
      try {
        section.node.click();
      } catch (_error) {
        continue;
      }
      await kdocsSleep(260);

      const directResult = kdocsExtractDocDirectResult(config, { allowPlainTextFallback: false });
      if (Array.isArray(directResult.items) && directResult.items.some(function (item) { return Boolean(item.anchorId); })) {
        directResult.countText = section.label
          ? ('栏目匹配: ' + directResult.items.length + '(' + section.label + ')')
          : ('栏目匹配: ' + directResult.items.length);
        return directResult;
      }
    }

    return {
      countText: '',
      rows: [],
      items: [],
      errorText: '',
    };
  }

  function kdocsBuildDocRowsResult(rows, label) {
    const items = kdocsUniqueItems(rows.map(function (row) {
      if (row && typeof row === 'object') {
        return {
          text: String(row.text || ''),
          anchorId: row.anchorId ? String(row.anchorId) : '',
        };
      }
      return { text: String(row || ''), anchorId: '' };
    }).filter(function (item) {
      return Boolean(item.text);
    }));

    return {
      countText: items.length > 0 ? (label + ': ' + items.length) : '',
      rows: items.map(function (item) {
        return item.text;
      }).slice(0, 20),
      items: items.slice(0, 20),
      errorText: '',
    };
  }

  function kdocsGetDocApiCandidates() {
    const candidates = [
      window.wpsInstance && window.wpsInstance.Application,
      window.instance && window.instance.Application,
      window.jssdk && window.jssdk.Application,
      window.webOfficeInstance && window.webOfficeInstance.Application,
      window.Application,
      window.WpsApplication,
      window.wpsApplication,
    ].filter(Boolean);

    return Array.from(new Set(candidates));
  }

  async function kdocsExtractDocApiResult(config) {
    const apps = kdocsGetDocApiCandidates();
    for (const app of apps) {
      try {
        const doc = await kdocsResolveValue(app.ActiveDocument);
        if (!doc) continue;

        const rows = [];
        const paragraphs = await kdocsResolveValue(doc.Paragraphs);
        const paragraphCount = paragraphs ? Number(await kdocsResolveValue(paragraphs.Count)) || 0 : 0;

        if (paragraphCount > 0 && typeof paragraphs.Item === 'function') {
          const limit = Math.min(paragraphCount, 400);
          for (let index = 1; index <= limit; index += 1) {
            const paragraph = await kdocsResolveValue(paragraphs.Item(index));
            if (!paragraph) continue;
            const range = await kdocsResolveValue(paragraph.Range);
            const text = range ? kdocsNormalizeResultText(await kdocsResolveValue(range.Text)) : '';
            if (!text) continue;
            rows.push({ text: text, anchorId: '' });
          }
        }

        let filteredRows = kdocsFilterKeywordItems(rows, config.keyword);
        if (filteredRows.length === 0 && typeof doc.Range === 'function') {
          const fullRange = await kdocsResolveValue(doc.Range(0, 200000));
          const fullText = fullRange ? await kdocsResolveValue(fullRange.Text) : '';
          filteredRows = kdocsFilterKeywordItems(kdocsSplitTextLines(fullText), config.keyword);
        }

        if (filteredRows.length > 0) {
          return kdocsBuildDocRowsResult(filteredRows, 'API匹配');
        }
      } catch (_error) {
        continue;
      }
    }

    return {
      countText: '',
      rows: [],
      errorText: '',
    };
  }

  function kdocsExtractDocDirectResult(config, options) {
    const settings = options || {};
    const directSelectors = [
      'p',
      'li',
      'h1',
      'h2',
      'h3',
      'h4',
      'blockquote',
      'pre',
      'td',
      'th',
      '[class*="paragraph"]',
      '[class*="para"]',
      '[class*="doc"] [class*="text"]',
      '[class*="editor"] [class*="text"]',
      '[class*="reader"] [class*="text"]',
      '[class*="page"] [class*="text"]',
      '[data-content]',
      '[data-contents]',
    ];

    const rows = [];
    for (const selector of directSelectors) {
      const nodes = document.querySelectorAll(selector);
      for (const node of nodes) {
        if (node.closest('#' + KDOCS_OVERLAY_ID)) continue;
        const text = kdocsNormalizeResultText(kdocsTextOf(node));
        if (!text || text.length < config.keyword.length) continue;
        if (text.length > 500) continue;
        if (!kdocsIncludesKeyword(text, config.keyword)) continue;
        if (kdocsIsNoiseLine(text, config.keyword)) continue;
        rows.push({ text: text, anchorId: kdocsEnsureMatchAnchor(node) });
      }
      if (rows.length >= 10) {
        break;
      }
    }

    if (rows.length === 0) {
      const genericMatches = kdocsFindGenericDocMatchItems(config);
      if (genericMatches.length > 0) {
        rows.push.apply(rows, genericMatches);
      }
    }

    if (rows.length === 0 && settings.allowPlainTextFallback !== false) {
      const bodyText = document.body ? String(document.body.innerText || '') : '';
      const lines = kdocsFilterKeywordLines(kdocsSplitTextLines(bodyText), config.keyword);
      rows.push.apply(rows, lines.slice(0, 20).map(function (line) {
        return { text: line, anchorId: '' };
      }));
    }

    return kdocsBuildDocRowsResult(rows, '正文匹配');
  }

  function kdocsExtractSearchResult(config) {
    const countText = kdocsTextOf(document.querySelector('.match-result-text'));
    const selectors = config.sourceType === 'doc'
      ? [
          '.db-global-find-result .db-global-find-select-list .select-item-value',
          '.db-global-find-result .select-item-value',
          '.db-global-find-result [class*="select-item-value"]',
          '.db-global-find-result [class*="snippet"]',
          '.db-global-find-result [class*="content"]',
          '.db-global-find-result [class*="item"]',
        ]
      : [
          '.db-global-find-result .db-global-find-select-list:not(.db-global-find-small-select-list) .select-item-value',
          '.db-global-find-result .db-global-find-select-list .select-item-value',
          '.db-global-find-result .select-item-value',
          '.db-global-find-result [class*="select-item-value"]',
        ];
    const rows = kdocsCollectTexts(selectors).filter(function (text) {
      return text && text !== countText && text !== config.keyword;
    });
    let uniqueRows = Array.from(new Set(rows));
    const errorText = Array.from(document.querySelectorAll('.db-global-find-modal-panel *'))
      .map(function (node) {
        return kdocsTextOf(node);
      })
      .find(function (text) {
        return text.indexOf('未找到') !== -1;
      }) || '';

    if (config.sourceType === 'doc' && uniqueRows.length === 0) {
      const directResult = kdocsExtractDocDirectResult(config);
      if (directResult.rows.length > 0) {
        return directResult;
      }
    }

    return {
      countText: countText,
      rows: uniqueRows.slice(0, 50),
      errorText: errorText,
    };
  }

  async function runKdocsAutoSearch(config) {
    if (window[KDOCS_RUNNING_FLAG]) return;
    window[KDOCS_RUNNING_FLAG] = true;

    renderKdocsSearchOverlay(config, {
      statusText: '正在自动搜索,请稍候...'
    });

    try {
      for (let attempt = 0; attempt < 60; attempt += 1) {
        kdocsCloseLoginModal();

        if (config.sourceType === 'doc') {
          const directAnchoredResult = kdocsExtractDocDirectResult(config, { allowPlainTextFallback: false });
          if (directAnchoredResult.rows.length > 0) {
            renderKdocsSearchOverlay(config, directAnchoredResult);
            return;
          }

          const sectionResult = await kdocsScanDocSectionsForMatches(config);
          if (sectionResult.rows.length > 0) {
            renderKdocsSearchOverlay(config, sectionResult);
            return;
          }

          const apiResult = await kdocsExtractDocApiResult(config);
          if (apiResult.rows.length > 0) {
            renderKdocsSearchOverlay(config, apiResult);
            return;
          }

          const directResult = kdocsExtractDocDirectResult(config);
          if (directResult.rows.length > 0) {
            renderKdocsSearchOverlay(config, directResult);
            return;
          }
        }

        const findButton = kdocsFindByText('button, [role="button"], span', '查找', true);
        if (findButton && !document.querySelector('.db-global-find-modal-panel')) {
          kdocsClick(findButton);
          await kdocsSleep(300);
        }

        const input = document.querySelector(
          '.db-global-find-keyword-setting input, '
          + '.db-global-find-keyword-setting textarea, '
          + 'input[placeholder*="查找"], '
          + 'textarea[placeholder*="查找"], '
          + 'input[type="search"]'
        );
        if (!input) {
          await kdocsSleep(300);
          continue;
        }

        kdocsSetInputValue(input, config.keyword);
        await kdocsSleep(150);

        const allSheetsLabel = config.sourceType === 'sheet' ? kdocsFindByText('label, span', '全部数据表', true) : null;
        if (allSheetsLabel) {
          kdocsClick(allSheetsLabel);
          await kdocsSleep(150);
        }

        const findAllButton = kdocsFindByText('button, [role="button"], span', '查找全部', true);
        if (findAllButton) {
          kdocsClick(findAllButton);
          await kdocsSleep(800);
        }

        for (let waitRound = 0; waitRound < 20; waitRound += 1) {
          const result = kdocsExtractSearchResult(config);
          if (result.countText || result.rows.length > 0 || result.errorText) {
            renderKdocsSearchOverlay(config, result);
            return;
          }
          await kdocsSleep(300);
        }
      }

      if (config.sourceType === 'doc') {
        const bodyLines = kdocsSplitTextLines(document.body ? String(document.body.innerText || '') : '');
        if (bodyLines.length > 20) {
          renderKdocsSearchOverlay(config, {
            statusText: '正文已加载,但未找到匹配内容。',
            countText: '正文扫描: 0',
            rows: [],
            errorText: '',
          });
          return;
        }
      }

      renderKdocsSearchOverlay(config, {
        statusText: '自动搜索超时,请点击 KDocs 页面的查找按钮再试一次。',
        countText: '',
        rows: [],
        errorText: '自动搜索超时',
      });
    } finally {
      window[KDOCS_RUNNING_FLAG] = false;
    }
  }

  async function openYellowrabbitViewer(title, apiUrl) {
    const win = window.open('about:blank', '_blank');

    if (!win) {
      window.open(apiUrl, '_blank', 'noopener');
      return;
    }

    writeViewerWindow(win, buildYellowrabbitViewerHtml({
      title: title,
      apiUrl: apiUrl,
      statusText: '加载中...',
      phase: 'loading',
      items: [],
      rawText: '',
      errorText: '',
    }));

    try {
      const response = await fetch(apiUrl, {
        method: 'GET',
        headers: { Accept: 'application/json' },
        credentials: 'omit',
      });

      if (!response.ok) {
        throw new Error('HTTP ' + response.status);
      }

      const text = await response.text();

      let payload = null;
      try {
        payload = JSON.parse(text);
      } catch (_error) {
        throw new Error('返回不是合法 JSON');
      }

      const items = extractYellowrabbitItems(payload);

      writeViewerWindow(win, buildYellowrabbitViewerHtml({
        title: title,
        apiUrl: apiUrl,
        statusText: '共 ' + items.length + ' 条结果',
        phase: 'done',
        items: items,
        rawText: text,
        errorText: '',
      }));
    } catch (error) {
      writeViewerWindow(win, buildYellowrabbitViewerHtml({
        title: title,
        apiUrl: apiUrl,
        statusText: '加载失败',
        phase: 'error',
        items: [],
        rawText: '',
        errorText: error && error.message ? error.message : '未知错误',
      }));
    }
  }

  function writeViewerWindow(win, html) {
    if (!win || win.closed) return;
    win.document.open();
    win.document.write(html);
    win.document.close();
  }

  function buildExternalAutoSearchUrl(baseUrl, mode, keyword) {
    const url = new URL(baseUrl);
    url.hash = new URLSearchParams({
      doubanMode: mode,
      doubanKeyword: keyword,
    }).toString();
    return url.toString();
  }

  function openAutoSubmitWindow(config) {
    const url = String((config && config.url) || '');
    if (!url) return;
    window.open(url, '_blank', 'noopener');
  }

  function openLemonSearch(keyword) {
    openAutoSubmitWindow({
      url: buildExternalAutoSearchUrl('https://lemonun.top/', 'lemonSearch', keyword),
    });
  }

  function open6vMovieSearch(keyword) {
    openAutoSubmitWindow({
      url: buildExternalAutoSearchUrl('https://www.6v520.tv/sousuo.html', 'sixvSearch', keyword),
    });
  }

  function parseExternalAutoSearchConfig() {
    const hash = String(window.location.hash || '');
    const hashParams = hash.startsWith('#') ? new URLSearchParams(hash.slice(1)) : null;
    const searchParams = new URLSearchParams(window.location.search || '');
    const mode = (hashParams && hashParams.get('doubanMode')) || searchParams.get('doubanMode') || '';
    const keyword = normalizeTitle((hashParams && hashParams.get('doubanKeyword')) || searchParams.get('doubanKeyword') || '');
    if (!mode || !keyword) return null;
    return { mode: mode, keyword: keyword };
  }

  function ensureHiddenFormField(form, name, value) {
    let input = form.querySelector('input[name="' + name + '"]');
    if (!input) {
      input = document.createElement('input');
      input.type = 'hidden';
      input.name = name;
      form.appendChild(input);
    }
    input.value = String(value);
  }

  function scheduleExternalAutoSubmit(submitter) {
    let attempts = 0;

    function trySubmit() {
      if (submitter()) return;
      attempts += 1;
      if (attempts < 40) {
        window.setTimeout(trySubmit, 150);
      }
    }

    trySubmit();
  }

  function runLemonAutoSearch(config) {
    scheduleExternalAutoSubmit(function () {
      const form = document.querySelector('#search-form');
      const input = form && form.querySelector('input[name="keyword"]');
      if (!form || !input) return false;

      kdocsSetInputValue(input, config.keyword);
      form.submit();
      return true;
    });
  }

  function runSixvAutoSearch(config) {
    scheduleExternalAutoSubmit(function () {
      const form = document.querySelector('#searchform');
      const input = form && form.querySelector('input[name="keyboard"]');
      if (!form || !input) return false;

      ensureHiddenFormField(form, 'show', 'title,smalltext');
      ensureHiddenFormField(form, 'tempid', '1');
      ensureHiddenFormField(form, 'x', '0');
      ensureHiddenFormField(form, 'y', '0');

      const tbnameField = form.querySelector('[name="tbname"]');
      if (tbnameField) {
        tbnameField.value = 'article';
      } else {
        ensureHiddenFormField(form, 'tbname', 'article');
      }

      kdocsSetInputValue(input, config.keyword);
      form.submit();
      return true;
    });
  }

  function extractYellowrabbitItems(payload) {
    if (Array.isArray(payload)) return payload;
    if (payload && Array.isArray(payload.data)) return payload.data;
    return [];
  }

  function getHttpLinks(item) {
    const links = [];
    for (const key in item) {
      if (!Object.prototype.hasOwnProperty.call(item, key)) continue;
      const value = item[key];
      if (typeof value === 'string' && /^https?:\/\//i.test(value)) {
        links.push({ key: key, url: value });
      }
    }
    return links;
  }

  function renderYellowrabbitItems(items, phase) {
    if (phase === 'loading') {
      return '<article class="item"><div class="meta">正在请求数据,请稍候...</div></article>';
    }

    if (!Array.isArray(items) || items.length === 0) {
      return '<article class="item"><div class="meta">暂无结果</div></article>';
    }

    return items.map(function (item, index) {
      const idText = item && item.id != null ? String(item.id) : String(index + 1);
      const titleText = item && item.title ? String(item.title) : '未命名资源';
      const titleDisplay = titleText.length > 85 ? titleText.slice(0, 85) + '...' : titleText;
      const links = getHttpLinks(item || {});

      const linkHtml = links.length > 0
        ? links.map(function (entry) {
            return '<a class="link" target="_blank" rel="noopener noreferrer" href="' + escapeHtml(entry.url) + '">' + escapeHtml(entry.key) + '</a>';
          }).join('')
        : '<span class="meta">无可用链接</span>';

      return '<article class="item">'
        + '<div class="item-row">'
        +   '<div class="item-main">'
        +     '<div class="meta">#' + escapeHtml(idText) + '</div>'
        +     '<h2 class="item-title">' + escapeHtml(titleDisplay) + '</h2>'
        +   '</div>'
        +   '<div class="links">' + linkHtml + '</div>'
        + '</div>'
        + '</article>';
    }).join('');
  }

  function buildYellowrabbitViewerHtml(options) {
    const title = options.title;
    const apiUrl = options.apiUrl;
    const statusText = options.statusText;
    const phase = options.phase;
    const items = options.items;
    const rawText = options.rawText;
    const errorText = options.errorText;

    const listHtml = renderYellowrabbitItems(items, phase);
    const errorHtml = errorText
      ? '<div class="error">错误: ' + escapeHtml(errorText) + '</div>'
      : '';
    const rawHtml = rawText ? escapeHtml(rawText) : '';

    return `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>yellowrabbit 搜索结果</title>
  <style>
    :root {
      color-scheme: light;
      --bg: #f6f8fc;
      --card: #ffffff;
      --line: #dfe5ef;
      --text: #111827;
      --muted: #5f6b7a;
      --primary: #1565c0;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      padding: 8px;
      font: 14px/1.6 -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif;
      background: var(--bg);
      color: var(--text);
    }
    .header {
      width: 55%;
      margin: 0 auto 6px;
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 9px;
      padding: 8px 10px;
    }
    .title {
      font-size: 17px;
      font-weight: 700;
      margin: 0 0 2px;
    }
    .sub {
      color: var(--muted);
      word-break: break-all;
    }
    .status {
      margin-top: 4px;
      color: var(--muted);
    }
    .error {
      margin-top: 6px;
      color: #b91c1c;
      font-weight: 600;
      word-break: break-word;
    }
    .list {
      width: 55%;
      margin: 0 auto;
      display: grid;
      gap: 6px;
    }
    .item {
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 9px;
      padding: 7px 9px;
    }
    .item-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }
    .item-main {
      min-width: 0;
      flex: 1;
    }
    .item-title {
      font-size: 14px;
      font-weight: 600;
      margin: 0;
      word-break: break-word;
      line-height: 1.35;
    }
    .meta {
      color: var(--muted);
      margin-bottom: 2px;
    }
    .links {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      justify-content: flex-end;
      align-items: center;
      max-width: 42%;
    }
    .link {
      border: 1px solid #c8d7ef;
      color: var(--primary);
      text-decoration: none;
      background: #f1f6ff;
      border-radius: 999px;
      padding: 6px 16px;
      font-size: 15px;
      font-weight: 600;
      line-height: 1.1;
      white-space: nowrap;
    }
    .link:hover {
      background: #e3efff;
    }
    details {
      width: 55%;
      margin: 8px auto 0;
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 10px;
      padding: 6px 10px;
    }
    pre {
      margin: 6px 0 0;
      white-space: pre-wrap;
      word-break: break-word;
      max-height: 320px;
      overflow: auto;
      color: #1f2937;
    }
    @media (max-width: 900px) {
      .header,
      .list,
      details {
        width: 96%;
      }
      .item-row {
        align-items: flex-start;
        flex-direction: column;
        gap: 6px;
      }
      .links {
        justify-content: flex-start;
        max-width: 100%;
      }
    }
  </style>
</head>
<body>
  <section class="header">
    <h1 class="title">yellowrabbit 搜索结果</h1>
    <div class="sub">关键词: ${escapeHtml(title)}</div>
    <div class="sub">API: <a class="link" target="_blank" rel="noopener noreferrer" href="${escapeHtml(apiUrl)}">打开原始 JSON</a></div>
    <div class="status">${escapeHtml(statusText)}</div>
    ${errorHtml}
  </section>

  <section class="list">${listHtml}</section>

  <details>
    <summary>原始 JSON</summary>
    <pre>${rawHtml}</pre>
  </details>
</body>
</html>`;
  }

  function ensureStyle() {
    if (document.querySelector(`.${STYLE_CLASS}`)) return;

    const style = document.createElement('style');
    style.className = STYLE_CLASS;
    style.textContent = `
      .${CONTAINER_CLASS} {
        margin-top: 10px;
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
      }

      .${BTN_CLASS} {
        padding: 3px 10px;
        border: 1px solid #1565C0;
        border-radius: 3px;
        text-decoration: none;
        color: #ffffff;
        font-size: 18px;
        font-weight: normal;
        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        transition: all 0.2s ease;
        display: inline-block;
        vertical-align: middle;
        line-height: 1.5;
        /* background: linear-gradient(to bottom, #2196F3, #1976D2); */
      }

      .${BTN_CLASS}:hover {
        background: linear-gradient(to bottom, #64B5F6, #42A5F5);
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
        transform: translateY(-1px);
      }

      .${BTN_CLASS}:active {
        transform: translateY(0);
        box-shadow: none;
      }

      .douban-source-manager-btn {
        color: #4d7c2f;
        border-style: dashed;
        border-color: #9fc37b;
        background: #f2f9e8;
      }

      .douban-source-manager-btn:hover {
        background: #e5f3d4;
        border-color: #86af61;
      }

      #${SOURCE_MANAGER_MODAL_ID}.douban-source-manager-mask {
        --douban-source-manager-accent: #5f9133;
        --douban-source-manager-accent-strong: #467121;
        --douban-source-manager-accent-soft: #edf6e3;
        --douban-source-manager-accent-soft-strong: #dcedc7;
        --douban-source-manager-border: #c8dbb6;
        --douban-source-manager-border-strong: #a7c48b;
        --douban-source-manager-text: #1c2c12;
        --douban-source-manager-muted: #5f7050;
        --douban-source-manager-panel-bg: linear-gradient(180deg, #f6faef 0%, #ffffff 18%, #f0f6e7 100%);
        --douban-source-manager-card-bg: linear-gradient(180deg, #ffffff 0%, #f4f8ea 100%);
        --douban-source-manager-card-hidden-bg: linear-gradient(180deg, #f4f6ef 0%, #eaf1df 100%);
        --douban-source-manager-shadow: 0 24px 54px rgba(43, 71, 20, 0.24);
        --douban-source-manager-ring: rgba(95, 145, 51, 0.22);
        position: fixed;
        inset: 0;
        z-index: 2147483647;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 20px;
        background: rgba(15, 23, 42, 0.45);
        backdrop-filter: blur(2px);
      }

      #${SOURCE_MANAGER_MODAL_ID} * {
        box-sizing: border-box;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-panel {
        width: min(860px, 100%);
        max-height: min(720px, calc(100vh - 40px));
        display: flex;
        flex-direction: column;
        gap: 14px;
        padding: 18px;
        overflow: hidden;
        border: 1px solid var(--douban-source-manager-border);
        border-radius: 18px;
        background: var(--douban-source-manager-panel-bg);
        box-shadow: var(--douban-source-manager-shadow);
        color: var(--douban-source-manager-text);
        font: 14px/1.55 -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-header,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toolbar,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-footer {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 10px;
        flex-wrap: wrap;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-title-wrap {
        min-width: 0;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-title {
        margin: 0;
        font-size: 22px;
        font-weight: 700;
        color: var(--douban-source-manager-accent-strong);
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-subtitle,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-summary {
        color: var(--douban-source-manager-muted);
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-subtitle {
        margin-top: 4px;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-summary {
        padding: 10px 12px;
        border: 1px solid var(--douban-source-manager-border);
        border-radius: 12px;
        background: var(--douban-source-manager-accent-soft);
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-close,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-action,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-secondary {
        appearance: none;
        border: 1px solid var(--douban-source-manager-border);
        border-radius: 999px;
        background: var(--douban-source-manager-accent-soft);
        color: var(--douban-source-manager-accent-strong);
        font: inherit;
        font-weight: 600;
        cursor: pointer;
        transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-close:hover,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-action:hover,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle:hover,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary:hover,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-secondary:hover {
        background: var(--douban-source-manager-accent-soft-strong);
        border-color: var(--douban-source-manager-border-strong);
        transform: translateY(-1px);
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-close {
        width: 38px;
        height: 38px;
        padding: 0;
        font-size: 22px;
        line-height: 1;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-action,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-secondary {
        padding: 8px 14px;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-list {
        margin: 0;
        padding: 0;
        list-style: none;
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
        align-content: start;
        gap: 12px;
        overflow: auto;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item {
        position: relative;
        display: block;
        min-height: 150px;
        padding: 12px;
        border: 1px solid var(--douban-source-manager-border);
        border-radius: 14px;
        background: var(--douban-source-manager-card-bg);
        box-shadow: 0 10px 22px rgba(95, 145, 51, 0.1);
        cursor: move;
        transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-hidden {
        background: var(--douban-source-manager-card-hidden-bg);
        border-style: dashed;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-dragging {
        opacity: 0.42;
        transform: scale(0.98);
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-drop-target,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-drop-after {
        border-color: var(--douban-source-manager-accent);
        box-shadow: 0 0 0 3px var(--douban-source-manager-ring), 0 14px 26px rgba(95, 145, 51, 0.18);
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-drop-target {
        transform: translateY(-2px);
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-drop-after {
        box-shadow: 0 0 0 3px var(--douban-source-manager-ring), inset 0 -4px 0 var(--douban-source-manager-accent), 0 14px 26px rgba(95, 145, 51, 0.18);
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-index {
        min-width: 36px;
        padding: 4px 8px;
        border-radius: 999px;
        background: var(--douban-source-manager-accent);
        color: #ffffff;
        text-align: center;
        font-weight: 700;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-main {
        min-width: 0;
        height: 100%;
        display: grid;
        grid-template-rows: auto 1fr;
        gap: 8px;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-name-group {
        display: grid;
        gap: 6px;
        min-width: 0;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item-top {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-label {
        font-size: 15px;
        font-weight: 700;
        color: var(--douban-source-manager-text);
        word-break: break-word;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-meta {
        font-size: 12px;
        color: var(--douban-source-manager-muted);
        word-break: break-word;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-rename {
        width: 100%;
        padding: 8px 10px;
        border: 1px solid var(--douban-source-manager-border);
        border-radius: 10px;
        background: rgba(255, 255, 255, 0.92);
        color: var(--douban-source-manager-text);
        font: inherit;
        transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-rename:focus {
        outline: none;
        border-color: var(--douban-source-manager-accent);
        box-shadow: 0 0 0 3px var(--douban-source-manager-ring);
        background: #ffffff;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle.is-hidden,
      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-secondary {
        background: #ffffff;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary {
        border-color: var(--douban-source-manager-accent-strong);
        background: linear-gradient(180deg, #78b44d 0%, #5f9133 100%);
        color: #ffffff;
      }

      #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary:hover {
        background: linear-gradient(180deg, #89be61 0%, #4f7f2b 100%);
        border-color: var(--douban-source-manager-accent-strong);
      }

      @media (max-width: 720px) {
        #${SOURCE_MANAGER_MODAL_ID}.douban-source-manager-mask {
          padding: 12px;
        }

        #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-panel {
          padding: 14px;
          max-height: calc(100vh - 24px);
        }

        #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-list {
          grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
        }

        #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item {
          min-height: 138px;
        }

        #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle {
          padding-inline: 12px;
        }
      }
    `;

    document.head.appendChild(style);
  }

  function createButton(source, title, labelAliases) {
    const link = document.createElement('a');
    const url = source.buildUrl(title);
    const displayLabel = getSourceDisplayLabel(source.label, labelAliases);
    link.href = url;
    link.target = '_blank';
    link.rel = 'noopener noreferrer';
    link.textContent = displayLabel;
    link.className = BTN_CLASS;
    link.title = displayLabel + '(右键隐藏该源)';

    if (typeof source.onClick === 'function') {
      link.addEventListener('click', function (event) {
        event.preventDefault();
        source.onClick({ title: title, url: url, event: event });
      });
    }

    link.addEventListener('contextmenu', function (event) {
      event.preventDefault();
      const ok = window.confirm('隐藏源「' + displayLabel + '」?\n可通过“源设置”按钮恢复。');
      if (!ok) return;
      hideSource(source.label);
      renderButtons(true);
    });

    return link;
  }

  function createSourceManagerButton() {
    const manager = document.createElement('a');
    manager.href = 'javascript:void(0)';
    manager.target = '_self';
    manager.textContent = '源设置';
    manager.className = BTN_CLASS + ' douban-source-manager-btn';
    manager.title = '管理隐藏源、排序和显示名称';
    manager.addEventListener('click', function (event) {
      event.preventDefault();
      openSourceManager();
    });
    return manager;
  }

  function removeOldContainer() {
    const old = document.querySelector(`.${CONTAINER_CLASS}`);
    if (old) old.remove();
  }

  function renderButtons(force) {
    const h1Element = document.querySelector('h1');
    if (!h1Element) return;

    const title = getMovieTitle();
    if (!title) return;

    const searchKeyword = getSearchKeyword(title);
    if (!searchKeyword) return;

    if (!force && document.querySelector(`.${CONTAINER_CLASS}`)) return;

    ensureStyle();
    removeOldContainer();

    const container = document.createElement('div');
    container.className = CONTAINER_CLASS;

    const hiddenSources = loadHiddenSources();
    const labelAliases = loadSourceLabelAliases();

    for (const source of getOrderedSources()) {
      if (hiddenSources.has(source.label)) continue;
      container.appendChild(createButton(source, searchKeyword, labelAliases));
    }

    container.appendChild(createSourceManagerButton());

    h1Element.insertAdjacentElement('afterend', container);
  }

  function scheduleRender() {
    if (renderTimer) {
      clearTimeout(renderTimer);
    }
    renderTimer = window.setTimeout(renderButtons, 120);
  }

  const host = window.location.hostname;
  const isKdocsHost = host === 'www.kdocs.cn' || host === 'appdocs.wpscdn.cn';
  const isLemonHost = host === 'lemonun.top';
  const isSixvHost = host === 'www.6v520.tv';
  const kdocsConfig = isKdocsHost ? parseKdocsSearchConfig() : null;
  const externalAutoSearchConfig = (isLemonHost || isSixvHost) ? parseExternalAutoSearchConfig() : null;
  if (isKdocsHost) {
    if (kdocsConfig) {
      runKdocsAutoSearch(kdocsConfig);
    }
    return;
  }

  if (isLemonHost) {
    if (externalAutoSearchConfig && externalAutoSearchConfig.mode === 'lemonSearch') {
      runLemonAutoSearch(externalAutoSearchConfig);
    }
    return;
  }

  if (isSixvHost) {
    if (externalAutoSearchConfig && externalAutoSearchConfig.mode === 'sixvSearch') {
      runSixvAutoSearch(externalAutoSearchConfig);
    }
    return;
  }

  const observer = new MutationObserver(scheduleRender);
  observer.observe(document.body, { childList: true, subtree: true });

  scheduleRender();
})();