Greasy Fork

Greasy Fork is available in English.

LANraragi 推荐栏

基于标签为 LANraragi 阅读器下方推荐区:猜你喜欢 & 同作者

当前为 2026-01-07 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name               LANraragi 推荐栏
// @namespace     https://github.com/Kelcoin
// @version            1.1
// @description     基于标签为 LANraragi 阅读器下方推荐区:猜你喜欢 & 同作者
// @author             Kelcoin
// @match              *://*/reader?id=*
// @grant               none
// @icon                 https://github.com/Difegue/LANraragi/raw/dev/public/favicon.ico
// @license            MIT
// ==/UserScript==

(function () {
    'use strict';

    // ==========================================
    // 配置
    // ==========================================
    const CONFIG = {
        // 每个视图最多显示多少个归档
        perViewLimit: 13,

        // 总共最多显示多少个归档(猜你喜欢 + 同作者)
        totalLimit: 26,

        // 是否在加载完成后自动展开
        autoExpand: false,

        // 用于“猜你喜欢”中随机选取标签的命名空间
        likeNamespaces: ['female', 'male', 'others'],

        // 如果 likeNamespaces 里一个都没有,就退而求其次
        likeFallbackNamespaces: ['character', 'parody'],

        // Search API 基础路径(相对当前站点)
        apiBase: '/api/search',

        // 是否在加载时打印 debug 信息
        debug: false
    };

    // ==========================================
    // 样式:阅读器下方的卡片区域
    // ==========================================
    const style = document.createElement('style');
        style.textContent = `
        #lrr-rec-app-wrapper {
            width: 100%;
            margin: 24px 0 0 0;
            box-sizing: border-box;
        }

        #lrr-rec-app {
            width: 100%;
            display: flex;
            justify-content: center;
            box-sizing: border-box;
        }

        #lrr-rec-container {
            width: 100%;
            max-width: 1400px;
            box-sizing: border-box;
            background: #1C1E24;
            backdrop-filter: blur(20px);
            -webkit-backdrop-filter: blur(20px);
            border: 1px solid rgba(140, 160, 190, 0.2);
            border-radius: 12px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
            display: flex;
            flex-direction: column;
            overflow: hidden;
            transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
            will-change: max-height;
            max-height: 340px;
            opacity: 1;
            transform: translateY(0);
        }

        #lrr-rec-container.collapsed {
            max-height: 46px;
            transition: max-height 0.35s cubic-bezier(0, 1, 0.5, 1), opacity 0.3s ease;
        }


        .lrr-rec-header {
            position: relative;
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 0 16px;
            height: 44px;
            min-height: 44px;
            flex: 0 0 44px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.05);
            background: #1C1E24;
            font-size: 14px;
            color: #e3e9f3;
            user-select: none;
            pointer-events: none;
        }

        #lrr-rec-container.collapsed .lrr-rec-header {
            border-bottom: 1px solid transparent;
        }

        .lrr-rec-tabs {
            display: flex;
            gap: 10px;
            align-items: center;
            height: 100%;
            pointer-events: auto;
        }

        #lrr-rec-status-msg {
            position: absolute;
            right: 48px;
            top: 50%;
            transform: translateY(-50%);
            font-size: 12px;
            color: #ffd54f;
            opacity: 0;
            transition: opacity 0.2s;
            pointer-events: none;
            white-space: nowrap;
        }
        #lrr-rec-status-msg.visible {
            opacity: 1;
        }

        .lrr-rec-tab-btn {
            background: transparent;
            border: 1px solid;
            color: var(--text-secondary, #a7b1c2);
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
            padding: 4px 8px;
            border-radius: 6px;
            transition: all 0.2s;
        }

        .lrr-rec-tab-btn:hover {
            background: #4a9ff0;
            color: #fff;
            border-color: rgba(206, 224, 255, 0.55) !important;
            transform: translateY(-1px);
        }

        .lrr-rec-tab-btn.active {
            background: #4a9ff0;
            color: #f7fbff !important;
            border-color: rgba(206, 224, 255, 0.55) !important;
            font-weight: 600 !important;
        }

        .lrr-rec-toggle {
            pointer-events: auto;
            cursor: pointer;
            color: #fff;
            opacity: 0.8;
            display: flex;
            align-items: center;
            justify-content: center;
            width: 32px;
            height: 32px;
            border-radius: 4px;
            transition: all 0.2s;
        }

        .lrr-rec-toggle:hover {
            opacity: 1;
            background: rgba(255, 255, 255, 0.1);
        }

        .lrr-rec-arrow-icon {
            display: none;
        }

        /* 箭头图标 */
        .lrr-rec-toggle::after {
            content: '';
            display: block;
            width: 0;
            height: 0;
            border-style: solid;
            transition: transform 0.3s ease;
            border-width: 6px 5px 0 5px;
            border-color: #e3e9f3 transparent transparent transparent;
            transform: rotate(0deg);
        }

        #lrr-rec-container.collapsed .lrr-rec-toggle::after {
            transform: rotate(180deg);
        }

        .lrr-rec-scroll-view {
            display: flex;
            flex-direction: row;
            overflow-x: auto;
            overflow-y: hidden;
            gap: 10px;
            padding: 12px 16px 14px;
            scroll-behavior: smooth;
            -webkit-overflow-scrolling: touch;
            scrollbar-width: none;
        }
        .lrr-rec-scroll-view::-webkit-scrollbar { display: none; }

        .lrr-rec-card {
            flex: 0 0 140px;
            display: flex;
            flex-direction: column;
            gap: 6px;
            text-decoration: none;
            position: relative;
            transition: transform 0.2s;
            color: inherit;
        }
        .lrr-rec-card:hover { transform: translateY(-3px); }

        .lrr-rec-thumb {
            width: 140px;
            height: 200px;
            border-radius: 8px;
            overflow: hidden;
            position: relative;
            background: #222;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }

        .lrr-rec-thumb-img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
            transition: transform 0.3s;
        }
        .lrr-rec-card:hover .lrr-rec-thumb-img { transform: scale(1.05); }

        .lrr-rec-title {
            font-size: 12px;
            color: #e3e9f3;
            line-height: 1.3;
            max-height: 32px;
            overflow: hidden;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
        }

        .lrr-rec-tags {
            position: absolute;
            bottom: 0;
            left: 0;
            width: 100%;
            height: auto;
            max-height: 100%;
            padding: 24px 4px 4px 4px;
            background: linear-gradient(to top, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.6) 60%, rgba(0, 0, 0, 0.0) 100%);
            display: flex;
            flex-direction: column-reverse;
            gap: 3px;
            box-sizing: border-box;
            pointer-events: none;
        }

        .lrr-rec-row {
            display: flex;
            flex-direction: row;
            flex-wrap: nowrap;
            gap: 3px;
            width: 100%;
            min-width: 0;
        }

        .lrr-rec-row:empty {
            display: none;
        }

        .lrr-rec-tag {
            display: inline-flex;
            align-items: center;
            font-size: 9px;
            padding: 1px 4px;
            border-radius: 3px;
            color: #fff;
            background: rgba(255, 255, 255, 0.18);
            backdrop-filter: blur(4px);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            flex: 0 1 auto;
            min-width: 0;
            max-width: 100%;
        }

        .lrr-rec-tag.lrr-rec-tag-match {
            box-shadow: 0 0 4px rgba(255, 255, 255, 0.35);
            background: rgba(255, 255, 255, 0.25);
        }

        .lrr-rec-view-hidden {
            display: none !important;
        }
        .lrr-rec-loading {
            padding: 24px 12px;
            color: #aaa;
            font-style: italic;
            font-size: 13px;
        }
        `;
    document.head.appendChild(style);

    // ==========================================
    // 工具函数
    // ==========================================

    function logDebug(...args) {
        if (CONFIG.debug) {
            console.log('[LRR Rec]', ...args);
        }
    }

    // --- 高权重标签表 ---
    // 在相似度判定时,命中这些标签会获得更高的权重分数
    // 你可以在这里添加你认为更能决定相似度的标签(如特定角色、特定play等)
    const HIGH_WEIGHT_TAGS = new Set([
        'female:netorare', 'female:mind control', 'female:corruption',
        'female:tomboy', 'female:big breast', 'female:swimsuit',
        'female:pantyhose', 'female:stockings', 'female:lolicon',
        'female:harem', 'female:futanari', 'female:anal',
        'female:public use', 'female:bbw', 'female:yuri',
        'female:anal intercourse', 'female:paizuri', 'female:dark skin',
        'female:huge breasts', 'female:dickgirl on female', 'female:hairy',
        'male:netorare', 'male:tomgirl', 'male:harem',
        'male:shotacon', 'male:gender change', 'male:virginity',
        'mixed:incest', 'mixed:group'
    ]);

    // 从 DOM 获取当前归档标签 + 显示文本
    function getCurrentArchiveTags() {
        const tagElements = document.querySelectorAll('#tagContainer .gt a');

        const tags = new Set();
        const tagsLower = new Set();
        const artistTags = new Set();
        const artistTagsLower = new Set();
        const categoryTags = new Set();
        const categoryTagsLower = new Set();
        const displayTextMap = new Map();

        // --- 黑名单标签列表 ---
        const blacklistedTags = [
            'other:extraneous ads',
            // 你可以在这里添加更多想屏蔽的具体标签
        ];

        tagElements.forEach(el => {
            let rawTag = el.getAttribute('search') || '';
            if (!rawTag && el.href && el.href.includes('q=')) {
                try {
                    rawTag = decodeURIComponent(el.href.split('q=')[1]);
                } catch (e) { /* ignore */ }
            }
            rawTag = rawTag.replace(/"/g, '').trim();

            if (!rawTag) return;

            const lowerKey = rawTag.toLowerCase();

            // --- 修改:检查是否在黑名单中 ---
            if (blacklistedTags.includes(lowerKey)) {
                return; // 如果在黑名单中,直接跳过,不添加到集合中
            }

            tags.add(rawTag);
            tagsLower.add(lowerKey);

            const prefix = rawTag.split(':')[0].toLowerCase();
            if (prefix === 'artist' || prefix === 'group') {
                artistTags.add(rawTag);
                artistTagsLower.add(lowerKey);
            }
            if (prefix === 'category') {
                categoryTags.add(rawTag);
                categoryTagsLower.add(lowerKey);
            }

            const displayText = (el.textContent || '').trim() || rawTag.split(':')[1] || rawTag;
            displayTextMap.set(lowerKey, displayText);
        });

        return {
            all: tags,
            allLower: tagsLower,
            artists: artistTags,
            artistsLower: artistTagsLower,
            categories: categoryTags,
            categoriesLower: categoryTagsLower,
            displayTextMap
        };
    }

    // 基于 Search API 搜索归档
    async function searchArchives(filter) {
        const params = new URLSearchParams();
        params.set('category', '');
        if (filter) params.set('filter', filter);
        params.set('start', '-1');
        params.set('sortby', 'title');
        params.set('order', 'asc');

        const url = `${CONFIG.apiBase}?${params.toString()}`;
        logDebug('Search:', url);

        try {
            const res = await fetch(url);
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            const json = await res.json();
            return Array.isArray(json.data) ? json.data : [];
        } catch (e) {
            console.error('LRR Rec: searchArchives error', e);
            return [];
        }
    }

    function shuffle(arr) {
        const a = arr.slice();
        for (let i = a.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [a[i], a[j]] = [a[j], a[i]];
        }
        return a;
    }

    function sample(arr, n) {
        if (arr.length <= n) return arr.slice();
        return shuffle(arr).slice(0, n);
    }

    function archiveHasSameCategory(archiveTagsStr, currentCategoryLowerSet) {
        if (!archiveTagsStr || currentCategoryLowerSet.size === 0) return false;
        const lowers = archiveTagsStr.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);
        return lowers.some(t => currentCategoryLowerSet.has(t));
    }

    function getMatchedSetForArchive(archiveTagsStr, sourceTagsLower) {
        const matched = new Set();
        if (!archiveTagsStr) return matched;
        archiveTagsStr.split(',').map(t => t.trim()).filter(Boolean).forEach(t => {
            const lowerKey = t.toLowerCase();
            if (sourceTagsLower.has(lowerKey)) {
                matched.add(lowerKey);
            }
        });
        return matched;
    }

    // --- 相似度计算函数 ---
    function calculateArchiveSimilarity(sourceTagsLower, candidateTagsStr) {
        if (!candidateTagsStr) return 0;

        const candidateTags = candidateTagsStr.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);
        let score = 0;

        candidateTags.forEach(tag => {
            if (sourceTagsLower.has(tag)) {
                // 基础分:命中一个标签得 1 分
                let points = 1;

                // 权重分:如果在高权重表中,额外加分
                if (HIGH_WEIGHT_TAGS.has(tag)) {
                    points += 3;
                }

                score += points;
            }
        });

        return score;
    }

    // 渲染标签 HTML
    function renderTags(tagsStr, sourceTagsLower, sourceDisplayTextMap, matchedSet) {
        if (!tagsStr) return '';

        const rawTags = tagsStr.split(',').map(t => t.trim()).filter(Boolean);
        if (rawTags.length === 0) return '';

        // 归一化命名空间
        const normalizeNs = ns =>
            (ns || 'other')
                .toLowerCase()
                .trim()
                .replace(/\s+/g, '')      // "date added" -> "dateadded"
                .replace(/_/g, '');       // "date_added" -> "dateadded"

        // 不显示的命名空间
        const hiddenNamespaces = [
            'category',
            'uploader',
            'source',
            'language',
            'timestamp',
            'dateadded' // 覆盖 date added / date-added / date_added
        ];

        const primaryNamespaces = ['female', 'male', 'others'];
        const secondaryNamespaces = ['parody', 'character', 'artist', 'group'];

        // 预处理 + 过滤隐藏命名空间
        const processed = rawTags
            .map((tag, index) => {
                const lowerKey = tag.toLowerCase();
                const parts = tag.split(':');
                const rawNs = parts.length > 1 ? parts[0] : 'other';
                const ns = normalizeNs(rawNs);

                if (hiddenNamespaces.includes(ns)) {
                    return null;
                }

                const rawValue = parts.length > 1 ? parts.slice(1).join(':') : tag;

                let displayText = rawValue || tag;

                if (sourceDisplayTextMap && sourceDisplayTextMap.has(lowerKey)) {
                    const mapped = sourceDisplayTextMap.get(lowerKey);
                    if (mapped && mapped !== rawValue) {
                        displayText = mapped;
                    }
                }

                return {
                    raw: tag,
                    ns,
                    displayText,
                    index,
                    length: displayText.length
                };
            })
            .filter(Boolean);

        if (processed.length === 0) return '';

        // 简单洗牌
        function shuffleLocal(arr) {
            const a = arr.slice();
            for (let i = a.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [a[i], a[j]] = [a[j], a[i]];
            }
            return a;
        }

        // 分组:primary / secondary / others
        const primary = [];
        const secondary = [];
        const others = [];
        for (const item of processed) {
            if (primaryNamespaces.includes(item.ns)) {
                primary.push(item);
            } else if (secondaryNamespaces.includes(item.ns)) {
                secondary.push(item);
            } else {
                others.push(item);
            }
        }

        const MAX_TAGS_PER_CARD = 5;
        const picked = [];

        // 1. 从 primary 随机取
        const primaryShuffled = shuffleLocal(primary);
        for (const t of primaryShuffled) {
            if (picked.length >= MAX_TAGS_PER_CARD) break;
            picked.push(t);
        }

        // 2. 不足 5 个,从 secondary 随机补
        if (picked.length < MAX_TAGS_PER_CARD && secondary.length > 0) {
            const secondaryShuffled = shuffleLocal(secondary);
            for (const t of secondaryShuffled) {
                if (picked.length >= MAX_TAGS_PER_CARD) break;
                picked.push(t);
            }
        }

        if (picked.length === 0) return '';

        // 排布优化:长标签优先,便于“下行更满”
        picked.sort((a, b) => b.length - a.length);

        // —— 分成最多两行 —— //
        const bottomRow = [];
        const topRow = [];
        let bottomLen = 0;
        let topLen = 0;

        for (const item of picked) {
            // 简单装箱:总长度更短的那行先放,保证底行尽量更满
            if (bottomLen <= topLen) {
                bottomRow.push(item);
                bottomLen += item.length;
            } else {
                topRow.push(item);
                topLen += item.length;
            }
        }

        // 行内顺序按原 index 排一下,避免完全乱序
        bottomRow.sort((a, b) => a.index - b.index);
        topRow.sort((a, b) => a.index - b.index);

        // 构造单个标签 HTML
        const buildTag = info => {
            const { raw, ns, displayText } = info;
            const nsClass = `${ns}-tag lrr-tag-${ns}`;
            const isMatch = matchedSet && matchedSet.has(raw.toLowerCase());
            const matchClass = isMatch ? 'lrr-rec-tag-match' : '';
            const spanCls = `${nsClass} lrr-rec-tag ${matchClass}`.trim();

            return (
                `<span class="${spanCls}"` +
                ` data-ns="${ns}"` +
                ` data-val="${raw}"` +
                ` search="${raw}"` +
                ` title="${raw}">` +
                `${displayText}` +
                `</span>`
            );
        };

        const rowsHtml = [];

        if (topRow.length > 0) {
            rowsHtml.push(
                `<div class="lrr-rec-row lrr-rec-row-top">` +
                topRow.map(buildTag).join('') +
                `</div>`
            );
        }

        if (bottomRow.length > 0) {
            rowsHtml.push(
                `<div class="lrr-rec-row lrr-rec-row-bottom">` +
                bottomRow.map(buildTag).join('') +
                `</div>`
            );
        }

        return rowsHtml.join('');
    }

    function createCardHTML(archive, matchedSet, sourceTagsLower, sourceDisplayTextMap) {
        const id = archive.arcid;
        const title = archive.title;
        const thumbUrl = `/api/archives/${id}/thumbnail`;
        const readerUrl = `/reader?id=${id}`;

        const tagsHtml = renderTags(
            archive.tags || '',
            sourceTagsLower,
            sourceDisplayTextMap,
            matchedSet || new Set()
        );

        return `
    <div class="lrr-rec-card" data-arcid="${id}">
        <a href="${readerUrl}" title="${title}">
            <div class="lrr-rec-thumb">
                <img src="${thumbUrl}" loading="lazy" alt="" class="lrr-rec-thumb-img" onerror="this.removeAttribute('onerror'); this.src='/img/no_thumb.png';">
                <div class="lrr-rec-tags">
                    ${tagsHtml}
                </div>
            </div>
            <div class="lrr-rec-title">${title}</div>
        </a>
    </div>
    `;
    }

    // ==========================================
    // 主逻辑
    // ==========================================
    async function init() {
        // --- 0. 扩展配置项:自动展开 ---
        if (typeof CONFIG.autoExpand === 'undefined') {
            CONFIG.autoExpand = false; // 默认禁用,用户可在脚本开头自行改为 true
        }

        const currentId = new URLSearchParams(window.location.search).get('id');
        if (!currentId) return;

        // --- 1. 构建布局(默认收起) ---
        const mainContainer = document.querySelector('.ido') || document.querySelector('#ido') || document.body;
        const wrapper = document.createElement('div');
        wrapper.id = 'lrr-rec-app-wrapper';

        const arrowSvg = `<svg class="lrr-rec-arrow-icon" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"></path></svg>`;

        const app = document.createElement('div');
        app.id = 'lrr-rec-app';
        app.innerHTML = `
            <div id="lrr-rec-container" class="collapsed">
                <div class="lrr-rec-header" id="lrr-rec-header-bar">
                    <div class="lrr-rec-tabs">
                        <button class="lrr-rec-tab-btn active" data-target="sim">猜你喜欢</button>
                        <button class="lrr-rec-tab-btn" data-target="artist" id="lrr-rec-btn-artist" style="display:none">同作者</button>
                    </div>
                    <span id="lrr-rec-status-msg">正在生成推荐...</span>
                    <div class="lrr-rec-toggle" title="展开/收起">
                        ${arrowSvg}
                    </div>
                </div>
                <div id="lrr-rec-view-sim" class="lrr-rec-scroll-view">
                    <div class="lrr-rec-loading">正在分析标签并搜索...</div>
                </div>
                <div id="lrr-rec-view-artist" class="lrr-rec-scroll-view lrr-rec-view-hidden"></div>
            </div>
        `;
        wrapper.appendChild(app);

        const footerElement = document.querySelector('p.ip');
        if (footerElement && footerElement.parentNode) {
            footerElement.parentNode.insertBefore(wrapper, footerElement);
        } else {
            mainContainer.appendChild(wrapper);
        }

        const container = document.getElementById('lrr-rec-container');
        const headerBar = document.getElementById('lrr-rec-header-bar');

        const btnSim = app.querySelector('[data-target="sim"]');
        const btnArtist = document.getElementById('lrr-rec-btn-artist');
        const viewSim = document.getElementById('lrr-rec-view-sim');
        const viewArtist = document.getElementById('lrr-rec-view-artist');

        // 初始收起
        viewSim.classList.add('lrr-rec-view-hidden');
        viewArtist.classList.add('lrr-rec-view-hidden');
        container.style.maxHeight = `${headerBar.offsetHeight}px`;

        const switchTab = (target) => {
            if (target === 'sim') {
                btnSim.classList.add('active');
                btnArtist.classList.remove('active');
                viewSim.classList.remove('lrr-rec-view-hidden');
                viewArtist.classList.add('lrr-rec-view-hidden');
            } else {
                btnSim.classList.remove('active');
                btnArtist.classList.add('active');
                viewSim.classList.add('lrr-rec-view-hidden');
                viewArtist.classList.remove('lrr-rec-view-hidden');
            }
        };
        btnSim.onclick = () => switchTab('sim');
        btnArtist.onclick = () => switchTab('artist');

        const enableHorizontalScroll = (el) => {
            el.addEventListener('wheel', (evt) => {
                if (evt.deltaY !== 0) {
                    evt.preventDefault();
                    el.scrollLeft += evt.deltaY;
                }
            }, { passive: false });
        };
        enableHorizontalScroll(viewSim);
        enableHorizontalScroll(viewArtist);

        const togglePanel = () => {
            const isCollapsed = container.classList.contains('collapsed');
            if (isCollapsed) {
                container.classList.remove('collapsed');
                const activeTarget = btnArtist.classList.contains('active') ? 'artist' : 'sim';
                switchTab(activeTarget);

                container.style.maxHeight = '';
            } else {
                container.classList.add('collapsed');
                container.style.maxHeight = '';
            }
        };

        headerBar.addEventListener('click', (e) => {
            if (e.target.closest('.lrr-rec-tab-btn')) return;
            togglePanel();
        });


        await new Promise(r => setTimeout(r, 800));

        const sourceData = getCurrentArchiveTags();
        const sourceTagsLower = sourceData.allLower;
        const sourceArtist = Array.from(sourceData.artists);
        const sourceCategoryLower = sourceData.categoriesLower;
        const sourceDisplayTextMap = sourceData.displayTextMap;

        logDebug('Current tags:', sourceData);

        let remainingTotal = CONFIG.totalLimit;

        async function buildYouMayLike() {
            if (remainingTotal <= 0) {
                viewSim.innerHTML = `<div class="lrr-rec-loading">推荐配额已满。</div>`;
                return;
            }
            const viewLimit = Math.min(CONFIG.perViewLimit, remainingTotal);

            const pickTags = (namespaces, fallbackNamespaces) => {
                const primary = [];
                const fallback = [];
                sourceData.all.forEach(raw => {
                    const parts = raw.split(':');
                    if (parts.length <= 1) return;
                    const ns = parts[0].toLowerCase();
                    if (namespaces.includes(ns)) primary.push(raw);
                    else if (fallbackNamespaces.includes(ns)) fallback.push(raw);
                });
                let base = primary.length > 0 ? primary : fallback;
                if (base.length === 0) return [];

                // --- 修改:基于标签总数动态调整搜索采样数 ---
                let maxSearchCount = 3;
                const totalTagsCount = sourceData.all.size;
                if (totalTagsCount > 40) {
                    maxSearchCount = 7;
                } else if (totalTagsCount > 20) {
                    maxSearchCount = 5;
                }

                const count = Math.min(maxSearchCount, Math.max(1, base.length));
                return sample(base, count);
            };

            const queryTags = pickTags(CONFIG.likeNamespaces, CONFIG.likeFallbackNamespaces);
            logDebug('Like query tags:', queryTags);

            if (!queryTags || queryTags.length === 0) {
                viewSim.innerHTML = `<div class="lrr-rec-loading">当前归档没有可用于推荐的标签。</div>`;
                return;
            }

            const allResultsMap = new Map();
            for (const tag of queryTags) {
                const filter = `${tag}$`;
                const data = await searchArchives(filter);
                data.forEach(arc => {
                    if (arc.arcid === currentId) return;
                    if (!allResultsMap.has(arc.arcid)) allResultsMap.set(arc.arcid, arc);
                });
            }

            let allResults = Array.from(allResultsMap.values());
            if (allResults.length === 0) {
                viewSim.innerHTML = `<div class="lrr-rec-loading">暂无基于当前标签的推荐结果。</div>`;
                return;
            }

            // --- 修改:计算相似度并分组 ---
            allResults.forEach(arc => {
                // 计算相似度分数并挂载到对象上
                arc._simScore = calculateArchiveSimilarity(sourceTagsLower, arc.tags || '');
            });

            const sameCategory = [];
            const otherCategory = [];

            allResults.forEach(arc => {
                if (archiveHasSameCategory(arc.tags || '', sourceCategoryLower)) {
                    sameCategory.push(arc);
                } else {
                    otherCategory.push(arc);
                }
            });

            // 排序函数:分数高的排前面
            const sortByScoreDesc = (a, b) => b._simScore - a._simScore;

            let picked = [];

            // 优先选取同分类内归档
            if (sameCategory.length > 0) {
                // 先对同分类按相似度排序
                sameCategory.sort(sortByScoreDesc);

                if (sameCategory.length >= viewLimit) {
                    // 如果同分类数量足够,直接截取高分的
                    picked = sameCategory.slice(0, viewLimit);
                } else {
                    // 如果同分类数量不足,全选,剩余名额从其他分类补
                    picked = [...sameCategory];
                    const needMore = viewLimit - picked.length;

                    if (otherCategory.length > 0) {
                        // 其他分类也按相似度排序
                        otherCategory.sort(sortByScoreDesc);
                        picked = picked.concat(otherCategory.slice(0, needMore));
                    }
                }
            } else {
                // 没有同分类,直接从其他分类按相似度选
                otherCategory.sort(sortByScoreDesc);
                picked = otherCategory.slice(0, viewLimit);
            }

            if (picked.length === 0) {
                viewSim.innerHTML = `<div class="lrr-rec-loading">暂无推荐结果。</div>`;
                return;
            }

            const html = picked.map(arc => {
                const matchedSet = getMatchedSetForArchive(arc.tags || '', sourceTagsLower);
                return createCardHTML(arc, matchedSet, sourceTagsLower, sourceDisplayTextMap);
            }).join('');

            viewSim.innerHTML = html;
            remainingTotal = Math.max(0, remainingTotal - picked.length);
        }

            // 同作者推荐
            async function buildSameAuthor() {
                if (remainingTotal <= 0) return;
                if (!Array.isArray(sourceArtist) || sourceArtist.length === 0) return;

                const viewLimit = Math.min(CONFIG.perViewLimit, remainingTotal);
                if (viewLimit <= 0) return;

                const allResultsMap = new Map();

                // 并行请求所有作者标签,避免串行等待
                const searchPromises = sourceArtist.map(tag => {
                    const filter = `${tag}$`;
                    return searchArchives(filter)
                        .then(data => ({ tag, data }))
                        .catch(e => {
                            logDebug('buildSameAuthor search error for tag:', tag, e);
                            return { tag, data: [] };
                        });
                });

                const allSearchResults = await Promise.all(searchPromises);

                // 汇总去重:按 arcid 去重,并排除当前归档
                for (const { tag, data } of allSearchResults) {
                    if (!Array.isArray(data) || data.length === 0) continue;

                    data.forEach(arc => {
                        if (!arc || arc.arcid == null) return;
                        if (arc.arcid === currentId) return; // 排除当前归档
                        if (!allResultsMap.has(arc.arcid)) {
                            allResultsMap.set(arc.arcid, arc);
                        }
                    });
                }

                const allResults = Array.from(allResultsMap.values());

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

                logDebug('buildSameAuthor found same-author archives:', allResults.length);

                // 打乱顺序后按 viewLimit 截断
                const shuffled = shuffle(allResults);
                const picked = shuffled.slice(0, viewLimit);

                const html = picked.map(arc => {
                    const matchedSet = getMatchedSetForArchive(arc.tags || '', sourceTagsLower);
                    return createCardHTML(arc, matchedSet, sourceTagsLower, sourceDisplayTextMap);
                }).join('');

                if (!html) return;

                viewArtist.innerHTML = html;
                btnArtist.style.display = 'block';
                btnArtist.innerText = '同作者';

                remainingTotal = Math.max(0, remainingTotal - picked.length);
            }

        try {
            await buildYouMayLike();
            await buildSameAuthor();

            if (CONFIG.autoExpand) {
                if (container.classList.contains('collapsed')) {
                    container.classList.remove('collapsed');
                    const activeTarget = btnArtist.classList.contains('active') ? 'artist' : 'sim';
                    switchTab(activeTarget);
                    container.style.maxHeight = '';
                }
            } else {
                const statusMsg = document.getElementById('lrr-rec-status-msg');
                if (statusMsg) {
                    statusMsg.innerText = '推荐已就绪';
                    setTimeout(() => { statusMsg.style.opacity = '0'; }, 3000);
                }
            }

            if (CONFIG.autoExpand) {
                const statusMsg = document.getElementById('lrr-rec-status-msg');
                if (statusMsg) statusMsg.style.display = 'none';
            }

        } catch (e) {
            console.error(e);
            viewSim.innerHTML = `<div class="lrr-rec-loading">加载失败</div>`;
        }
    }

    // 启动
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();