【自用】F95助手

①标签黑白名单。②标签汉化。③提取游戏信息。④跳转SteamDB、VNDB页面。⑤利用SteamDB/VNDB页面的内容补充信息。(更详细的功能请见页面介绍和代码内的注释)

// ==UserScript==
// @name                【自用】F95助手
// @name:en             F95 Helper
// @namespace           https://greasyfork.org/users/1215910
// @icon                https://www.google.com/s2/favicons?sz=64&domain=f95zone.to
// @version             4.1.3
// @description         ①标签黑白名单。②标签汉化。③提取游戏信息。④跳转SteamDB、VNDB页面。⑤利用SteamDB/VNDB页面的内容补充信息。(更详细的功能请见页面介绍和代码内的注释)
// @description:en      This plugin is designed for Chinese players. Some of its features may not be suitable for native English speakers. Of course, if you like the other features, feel free to take the code and use it.
// @author              诉语
// @match               https://f95zone.to/threads/*
// @match               https://vndb.org/v*
// @match               https://steamdb.info/app/*
// @grant               GM_setValue
// @grant               GM_getValue
// @grant               GM_registerMenuCommand
// @homepage            https://greasyfork.org/scripts/550171
// @license             MIT
// ==/UserScript==


// 更新时间:2025-09-23
// 更新内容:v1.0 复刻和修改功能。自定义标签的黑/白名单。
//      v1.1 新增功能。对所有标签进行汉化。
//      v1.2 新增功能。提供按钮,用来显示和复制游戏信息。
//      v1.3.0 新增功能。提供按钮,用来在 Steam 和 VNDB 上搜索当前游戏名的游戏。
//      v1.3.1 完善功能。提取游戏信息功能中,新增AI标签的条目。
//      v1.3.2 美化界面。按钮提供更美观的悬停交互。
//      v1.3.3 完善功能,修复Bug。增加了标题的标签组,新增开发进度的复制条目。现在能更准确识别标题中的信息了。
//      v1.3.4 修复可能存在的Bug。改变了标题文本的提取方式,现在能更准确识别标题中的信息了。另外,统一了标题标签组的数据格式。
//      v2.0.0 大型更新。新增功能:数据可本地存储(点击复制按钮时存储)。新增多个数据条目。提供本地数据和实时网页数据的对比。扩充和新增了多个函数框架。其他本人定制的内容不作详述。
//      v3.0.0 大型更新。新增功能:现在可以利用 VNDB 页面的信息更新本地数据库。新增多个数据条目。重做和新增了多个函数框架。
//      v3.0.1 修复Bug。修复了F95评分人数超过1000人时只显示千位数的Bug。修复了两个网站按钮的文字样式不统一的Bug。
//      v4.0.0 大型更新。新增功能:现在可以利用SteamDB页面的信息更新本地数据库。新增多个SteamDB相关的数据条目。所有跳转Steam的按钮改为跳转SteamDB。新增和重做了多个函数框架。修复了大量Bug,但是可能新增了更多o(╥﹏╥)o。
//      v4.1.0 完善功能。“复制信息”按钮新增“讲介士”样式。现在复制的信息格式默认为“讲介士”样式(即飘窗的样式),如需切换回原来的Excel输出,请在设置中修改为“收集癖”样式。
//      v4.1.1 修复Bug。修复了SteamDB中评论数和评分的提取Bug。在这里提醒一下,3个网站上的评分全部选取的是最简单的算术平均分,而非网站的调整评分(VNDB采用贝叶斯平均,SteamDB采用统计学加权)。
//      v4.1.2 小型调整。VNDB 的平均分改为保留1位小数,与其他评分统一。飘窗的Steam文本进行了细微调整,与其他网站统一。
//      v4.1.3 修复Bug。重新修复了SteamDB中评论数的提取Bug。
// 其他计划:标签分类排序。搜集Patreon等地址。

(function() {
    'use strict';

    // ==================== 中英对照词典 ====================
    const tagTranslations = {
        "2d game": "2D游戏",
        "2dcg": "2D CG",
        "3d game": "3D游戏",
        "3dcg": "3D CG",
        "adventure": "冒险",
        "ahegao": "阿黑颜",
        "ai cg": "AI CG",
        "anal sex": "肛交",
        "animated": "动画",
        "asset-addon": "素材-插件",
        "asset-ai-shoujo": "素材-AI少女",
        "asset-animal": "素材-动物",
        "asset-animation": "素材-动画",
        "asset-bundle": "素材-合集",
        "asset-character": "素材-角色",
        "asset-clothing": "素材-服装",
        "asset-daz-gen1": "素材-Daz G1",
        "asset-daz-gen2": "素材-Daz G2",
        "asset-daz-gen3": "素材-Daz G3",
        "asset-daz-gen8": "素材-Daz G8",
        "asset-daz-gen81": "素材-Daz G8.1",
        "asset-daz-gen9": "素材-Daz G9",
        "asset-daz-m4": "素材-Daz M4",
        "asset-daz-v4": "素材-Daz V4",
        "asset-environment": "素材-环境",
        "asset-expression": "素材-表情",
        "asset-female": "素材-女性",
        "asset-hair": "素材-头发",
        "asset-hdri": "素材-HDRI",
        "asset-honey-select": "素材-甜心选择",
        "asset-honey-select2": "素材-甜心选择2",
        "asset-light": "素材-光照",
        "asset-male": "素材-男性",
        "asset-morph": "素材-捏脸",
        "asset-nonbinary": "素材-非二元性别",
        "asset-plugin": "素材-插件",
        "asset-pose": "素材-姿势",
        "asset-prop": "素材-道具",
        "asset-scene": "素材-场景",
        "asset-script": "素材-脚本",
        "asset-shader": "素材-着色器",
        "asset-texture": "素材-贴图",
        "asset-utility": "素材-工具",
        "asset-vehicle": "素材-载具",
        "bdsm": "BDSM",
        "bestiality": "兽交",
        "big ass": "大屁股",
        "big tits": "巨乳",
        "blackmail": "勒索",
        "blood": "血腥",
        "bukkake": "颜射",
        "censored": "有码",
        "character creation": "自定义角色",
        "cheating": "出轨",
        "combat": "战斗",
        "corruption": "腐化",
        "cosplay": "COS",
        "creampie": "内射",
        "dating sim": "恋爱模拟",
        "dilf": "熟男",
        "drugs": "药物",
        "dystopian setting": "反乌托邦背景",
        "exhibitionism": "暴露癖",
        "fantasy": "奇幻",
        "female domination": "女性支配/女王",
        "female protagonist": "女主",
        "footjob": "足交",
        "furry": "福瑞控",
        "futa": "扶他",
        "futa/trans": "扶他/变性",
        "futa/trans protagonist": "扶他/变性 主角",
        "gay": "男同",
        "graphic violence": "血腥暴力",
        "groping": "痴汉",
        "group sex": "群交",
        "handjob": "手交",
        "harem": "后宫",
        "horror": "恐怖",
        "humiliation": "羞辱",
        "humor": "幽默",
        "incest": "乱伦",
        "internal view": "断面图",
        "interracial": "异族",
        "japanese game": "日本游戏",
        "kinetic novel": "动态小说",
        "lactation": "乳汁",
        "lesbian": "女同",
        "loli": "萝莉",
        "male domination": "男性支配",
        "male protagonist": "男主",
        "management": "经营",
        "masturbation": "自慰",
        "milf": "熟女",
        "mind control": "精神控制",
        "mobile game": "手机游戏",
        "monster": "怪物",
        "monster girl": "兽娘/魔物娘",
        "multiple endings": "多结局",
        "multiple penetration": "双插/多插",
        "multiple protagonist": "多主角",
        "necrophilia": "恋尸癖",
        "netorare": "NTR",
        "no sexual content": "无H内容",
        "oral sex": "口交",
        "paranormal": "灵异",
        "parody": "恶搞",
        "platformer": "平台游戏",
        "point & click": "点击式",
        "possession": "附身",
        "pov": "第一人称视角",
        "pregnancy": "怀孕",
        "prostitution": "卖淫",
        "puzzle": "解谜",
        "rape": "强暴",
        "real porn": "真人视频",
        "religion": "宗教",
        "romance": "浪漫",
        "rpg": "RPG",
        "sandbox": "沙盒",
        "scat": "吃粪",
        "school setting": "校园背景",
        "sci-fi": "科幻",
        "sex toys": "性玩具",
        "sexual harassment": "性骚扰",
        "shooter": "射击游戏",
        "shota": "正太",
        "side-scroller": "横版卷轴",
        "simulator": "模拟器",
        "sissification": "伪娘改造",
        "slave": "奴隶",
        "sleep sex": "睡奸",
        "spanking": "打屁股",
        "strategy": "策略",
        "stripping": "脱衣舞",
        "superpowers": "超能力",
        "swinging": "换妻",
        "teasing": "挑逗",
        "tentacles": "触手",
        "text based": "文字游戏",
        "titfuck": "乳交",
        "trainer": "养成",
        "transformation": "变性",
        "trap": "伪娘",
        "turn based combat": "回合制战斗",
        "twins": "双胞胎",
        "urination": "圣水",
        "vaginal sex": "阴道交",
        "virgin": "处女",
        "virtual reality": "VR",
        "violence": "暴力",
        "voiced": "有配音",
        "vore": "吞食",
        "voyeurism": "窥视癖",
    };


    // ==================== 初始化变量 ====================
    let Like = GM_getValue('喜好的标签', '');
    let Dislike = GM_getValue('厌恶的标签', '');
    let Concern = GM_getValue('值得注意的标签', '');

    // ==================== 主逻辑 ====================
    GM_registerMenuCommand('设置', openSettings);

    if (isURL('f95zone')) {
        const $tagList = $('span.js-tagList');
        const likeTags = [], dislikeTags = [], concernTags = [], others = [];
        // 功能1+2:标签黑白名单+汉化
        $('span.js-tagList a').each(function() {
            const $this = $(this);
            const englishText = $this.text();
            const chineseText = tagTranslations[englishText] || englishText;
            $this.text(chineseText);

            if (includesAnyIgnoreCase(chineseText, Like)) {
                $this.css('color', '#90ee90'); // 设置“喜欢”的样式(浅绿)
                likeTags.push($this);
            } else if (includesAnyIgnoreCase(chineseText, Dislike)) {
                $this.css('color', '#ff0000'); // 设置“厌恶”的样式(红色)
                dislikeTags.push($this);
            } else if (includesAnyIgnoreCase(chineseText, Concern)) {
                $this.css('color', '#ffff00'); // 设置“值得注意”的样式(黄色)
                concernTags.push($this);
            } else {
                others.push($this); // 其他标签
            }
        });
        const $fragment = $(document.createDocumentFragment());
        likeTags.forEach($el => $fragment.append($el).append(' '));
        dislikeTags.forEach($el => $fragment.append($el).append(' '));
        concernTags.forEach($el => $fragment.append($el).append(' '));
        others.forEach($el => $fragment.append($el).append(' '));
        $tagList.empty().append($fragment);

        // 功能3+4:创建操作按钮 (复制和搜索)
        f95Buttons();
    }
    else if (isURL('vndb')){
        // 功能5:提取VNDB页面的内容
        vndbButtons();
    }
    else if (isURL('steamdb')){
        // 功能6:提取SteamDB页面的内容
        steamdbButtons();
    }


    // ==================== 函数定义区域 ====================
    // -------------------- 通用 --------------------
    // 设置插件的对话框窗口
    function openSettings() {
        if ($('#translationWindow').length) return; // 如果窗口已存在,则不重复创建
        // 读取布尔值设置,默认为 true (收集癖样式)
        let copyButtonOutputStyleSetting = GM_getValue('copyButtonOutputStyle', false);

        const translationWindow = document.createElement('div');
        translationWindow.id = 'translationWindow';
        translationWindow.style.position = 'fixed';
        translationWindow.style.top = '100px';
        translationWindow.style.left = '50%';
        translationWindow.style.transform = 'translateX(-50%)';
        translationWindow.style.background = '#272727';
        translationWindow.style.border = '1px solid #ccc';
        translationWindow.style.padding = '10px';
        translationWindow.style.zIndex = 99999;
        translationWindow.style.width = '500px';
        translationWindow.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';

        translationWindow.innerHTML = `
            <div id="dragHandle" style="cursor: move; margin-bottom:10px; font-weight:bold;color:#272727; background:#eee; padding:4px;">
                F95助手 设置
            </div>

            <div style="margin-bottom: 15px; border-bottom: 1px solid #555; padding-bottom: 10px;">
                <label style="color: #eee; display: block; margin-bottom: 8px;">“复制按钮”输出文本样式:</label>
                <div style="display: flex; gap: 20px;">
                    <label style="color: #ddd; cursor: pointer;">
                        <input type="radio" name="copyButtonOutputStyle" value="presenter" ${!copyButtonOutputStyleSetting ? 'checked' : ''}> “讲介士”样式
                    </label>
                    <label style="color: #ddd; cursor: pointer;">
                        <input type="radio" name="copyButtonOutputStyle" value="collection" ${copyButtonOutputStyleSetting ? 'checked' : ''}> “收集癖”样式
                    </label>
                </div>
            </div>

            <div style="margin-bottom: 5px; color: #ccc;">
                请输入中文标签,不同标签需要用英文逗号“,”分隔。
            </div>
            <div style="display:flex; margin-bottom:5px; align-items:flex-start; gap: 10px;">
                <label style="width: 70px; color:#90ee90; padding-top: 4px; flex-shrink: 0;">喜欢</label>
                <textarea id="likeInput" rows="4" style="flex-grow: 1; background:#606060; color:#ffffff; border:1px solid #777; resize:vertical;">${Like}</textarea>
            </div>
            <div style="display:flex; margin-bottom:10px; align-items:flex-start; gap: 10px;">
                <label style="width: 70px; color:#ff0000; padding-top: 4px; flex-shrink: 0;">厌恶</label>
                <textarea id="dislikeInput" rows="4" style="flex-grow: 1; background:#606060; color:#ffffff; border:1px solid #777; resize:vertical;">${Dislike}</textarea>
            </div>
            <div style="display:flex; margin-bottom:15px; align-items:flex-start; gap: 10px;">
                <label style="width: 70px; color:#ffff00; padding-top: 4px; flex-shrink: 0;">值得注意</label>
                <textarea id="concernInput" rows="4" style="flex-grow: 1; background:#606060; color:#ffffff; border:1px solid #777; resize:vertical;">${Concern}</textarea>
            </div>
            <div style="display:flex; justify-content:space-between;">
                <button id="saveBtn">保存</button>
                <button id="closeBtn">退出</button>
            </div>
        `;

        document.body.appendChild(translationWindow);

        makeDraggable(translationWindow, document.getElementById('dragHandle')); // 使窗口可拖动

        // 保存按钮
        document.getElementById('saveBtn').onclick = function() {
            // 保存 copyButtonOutputStyle 布尔值
            const selectedMode = document.querySelector('input[name="copyButtonOutputStyle"]:checked').value;
            const newcopyButtonOutputStyle = selectedMode === 'collection'; // 如果是'collection'则为true, 否则为false
            GM_setValue('copyButtonOutputStyle', newcopyButtonOutputStyle);
            // 清理和格式化用户输入的“标签”列表
            let likeVal = document.getElementById('likeInput').value.trim().replace(/^,|,$/g, '').split(',').map(s => s.trim()).filter(Boolean).join(',');
            let dislikeVal = document.getElementById('dislikeInput').value.trim().replace(/^,|,$/g, '').split(',').map(s => s.trim()).filter(Boolean).join(',');
            let concernVal = document.getElementById('concernInput').value.trim().replace(/^,|,$/g, '').split(',').map(s => s.trim()).filter(Boolean).join(',');
            // 更新全局变量
            Like = likeVal;
            Dislike = dislikeVal;
            Concern = concernVal;
            // 保存
            GM_setValue('喜好的标签', Like);
            GM_setValue('厌恶的标签', Dislike);
            GM_setValue('值得注意的标签', Concern);
            alert('设置已保存!');
            // 刷新
            location.reload();
        };
        // 关闭按钮
        document.getElementById('closeBtn').onclick = function() {
            translationWindow.remove();
        };
    }

    /**
     * 核心UI函数:负责注入CSS和创建所有通用的按钮及容器
     * @returns {object} 包含所有已创建的DOM元素的对象
     */
    function createButtonUI() {
        // --- 注入通用CSS样式 ---
        if (!document.getElementById('f95-helper-styles')) {
            const css = `
                /* --- 按钮基础样式 --- */
                .f95-helper-button {
                    transition: transform 0.2s ease, filter 0.2s ease; /* 定义平滑过渡效果,时长为0.2秒,缓动函数为 ease */
                    font-weight: bold;
                    text-align: center; /* 文字统一居中显示 */
                }
                /* --- 按钮悬停样式 --- */
                .f95-helper-button:hover {
                    filter: brightness(85%); /* 悬停时,按钮颜色变暗15% */
                    transform: scale(1.05); /* 悬停时,按钮放大5% */
                }
                /* --- 飘窗中分隔线的样式 --- */
                .tooltip-separator {
                    border-top: 1px dashed #777;
                    margin: 8px 0;
                }
            `;
            const styleSheet = document.createElement("style");
            styleSheet.id = 'f95-helper-styles'; // 添加ID防止重复注入
            styleSheet.innerText = css;
            document.head.appendChild(styleSheet);
        }

        // --- 创建通用UI元素 ---
        // 总容器
        const buttonContainer = document.createElement('div');
        Object.assign(buttonContainer.style, {
            position: 'fixed', top: '150px', right: '20px', zIndex: '10000',
            display: 'flex', flexDirection: 'column', gap: '8px'
        });
        document.body.appendChild(buttonContainer);
        // 通用的按钮样式
        const baseButtonStyle = {
            padding: '8px 12px', color: 'white', border: 'none',
            borderRadius: '5px', cursor: 'pointer',
            boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
        };
        // 悬浮预览窗口
        const tooltip = document.createElement('div');
        Object.assign(tooltip.style, {
            position: 'fixed', display: 'none', zIndex: '10001', padding: '10px',
            backgroundColor: 'rgba(40, 40, 40, 0.95)', color: 'white', border: '1px solid #555',
            borderRadius: '5px', pointerEvents: 'none', lineHeight: '1.6',
            boxShadow: '0 4px 8px rgba(0,0,0,0.4)', fontFamily: 'sans-serif',
            fontSize: '14px', whiteSpace: 'nowrap'
        });
        document.body.appendChild(tooltip);

        return { buttonContainer, baseButtonStyle, tooltip };
    }

    // 飘窗UI函数
    function buttonTooltip(tooltipElement, buttonElement, site) {
        let liveInfo = null;
        let localInfo = null;
        let matchedF95ThreadId = null;

        if (site === 'f95') {
            liveInfo = f95GameInfo();
            matchedF95ThreadId = liveInfo.f95ThreadId;
        }
        else if (site === 'vndb'){
            liveInfo = vndbGameInfo();
            matchedF95ThreadId = vndbMatchDB(liveInfo);
        }
        else if (site === 'steamdb'){
            liveInfo = steamdbGameInfo();
            matchedF95ThreadId = steamdbMatchDB(liveInfo);
        }
        if (!liveInfo) return;
        if (matchedF95ThreadId) localInfo = getLocalInfo(matchedF95ThreadId);

        const result = dataCompare(localInfo, liveInfo);
        const { local, live, compare } = result;

        // 辅助函数:创建一个带颜色的span标签
        const createColoredSpan = (text, color) => `<span style="color: ${color};">${text}</span>`;
        const NA_TEXT = 'N/A'; // 统一的 "N/A" 文本
        const COLOR_GREEN = '#73d791'; // 薄荷绿
        const COLOR_RED = '#f47272'; // 珊瑚红
        // const COLOR_GREEN = '#5eead4'; // 青
        // const COLOR_RED = '#f5d442'; // 橙黄

        // 【微调地点】飘窗显示
        // --- 名称信息 ---
        const rows = {};
        rows.line0 = `<div class="tooltip-separator"></div>`;
        // 英文名称(必须存在,没有则显示错误)
        switch (compare.gameName1) {
            case '00': rows.gameName1 = `<b>英文名称:</b>${NA_TEXT}<br>`; break;
            case '01': rows.gameName1 = `<b>英文名称:</b>${createColoredSpan(live.gameName1, COLOR_GREEN)}<br>`; break;
            case '10': rows.gameName1 = `<b>英文名称:</b>${local.gameName1}<br>`; break;
            case '11': rows.gameName1 = `<b>英文名称:</b>${local.gameName1}<br>`; break;
            case '99': rows.gameName1 = `<b>英文名称:</b>${createColoredSpan(local.gameName1, COLOR_RED)} ⇒ ${createColoredSpan(live.gameName1, COLOR_GREEN)}<br>`; break;
        }
        // 中文名称(不一定存在,没有则留空)
        switch (compare.gameName2) {
            case '00': rows.gameName2 = `<b>中文名称:</b><br>`; break;
            case '01': rows.gameName2 = `<b>中文名称:</b>${createColoredSpan(live.gameName2, COLOR_GREEN)}<br>`; break;
            case '10': rows.gameName2 = `<b>中文名称:</b>${local.gameName2}<br>`; break;
            case '11': rows.gameName2 = `<b>中文名称:</b>${local.gameName2}<br>`; break;
            case '99': rows.gameName2 = `<b>中文名称:</b>${createColoredSpan(local.gameName2, COLOR_RED)} ⇒ ${createColoredSpan(live.gameName2, COLOR_GREEN)}<br>`; break;
        }
        // 官方日文名称(官中、官英都不存在时才显示)
        if (!local.gameName1Official && !local.gameName2Official){
            switch (compare.gameName3) {
                case '00': break;
                case '01': rows.gameName3 = `<b>日文名称:</b>${createColoredSpan(live.gameName3, COLOR_GREEN)}<br>`; break;
                case '10': rows.gameName3 = `<b>日文名称:</b>${local.gameName3}<br>`; break;
                case '11': rows.gameName3 = `<b>日文名称:</b>${local.gameName3}<br>`; break;
                case '99': rows.gameName3 = `<b>日文名称:</b>${createColoredSpan(local.gameName3, COLOR_RED)} ⇒ ${createColoredSpan(live.gameName3, COLOR_GREEN)}<br>`; break;
            }
        }
        // 作者(必须存在,没有则显示错误)
        switch (compare.gameDev) {
            case '00': rows.gameDev = `<b>作  者:</b>${NA_TEXT}<br>`; break;
            case '01': rows.gameDev = `<b>作  者:</b>${createColoredSpan(live.gameDev, COLOR_GREEN)}<br>`; break;
            case '10': rows.gameDev = `<b>作  者:</b>${local.gameDev}<br>`; break;
            case '11': rows.gameDev = `<b>作  者:</b>${local.gameDev}<br>`; break;
            case '99': rows.gameDev = `<b>作  者:</b>${createColoredSpan(local.gameDev, COLOR_RED)} ⇒ ${createColoredSpan(live.gameDev, COLOR_GREEN)}<br>`; break;
        }
        // 版本(必须存在,没有则显示错误)
        switch (compare.gameVersion) {
            case '00': rows.gameVersion = `<b>版  本:</b>${NA_TEXT}<br>`; break;
            case '01': rows.gameVersion = `<b>版  本:</b>${createColoredSpan(live.gameVersion, COLOR_GREEN)}<br>`; break;
            case '10': rows.gameVersion = `<b>版  本:</b>${local.gameVersion}<br>`; break;
            case '11': rows.gameVersion = `<b>版  本:</b>${local.gameVersion}<br>`; break;
            case '99': rows.gameVersion = `<b>版  本:</b>${createColoredSpan(local.gameVersion, COLOR_RED)} ⇒ ${createColoredSpan(live.gameVersion, COLOR_GREEN)}<br>`; break;
        }
        // 开发进度(必须存在,没有则显示错误)
        switch (compare.gameDevStatus) {
            case '00': rows.gameDevStatus = `<b>开发进度:</b>${NA_TEXT}<br>`; break;
            case '01': rows.gameDevStatus = `<b>开发进度:</b>${createColoredSpan(live.gameDevStatus, COLOR_GREEN)}<br>`; break;
            case '10': rows.gameDevStatus = `<b>开发进度:</b>${local.gameDevStatus}<br>`; break;
            case '11': rows.gameDevStatus = `<b>开发进度:</b>${local.gameDevStatus}<br>`; break;
            case '99': rows.gameDevStatus = `<b>开发进度:</b>${createColoredSpan(local.gameDevStatus, COLOR_RED)} ⇒ ${createColoredSpan(live.gameDevStatus, COLOR_GREEN)}<br>`; break;
        }
        // 更新时间(必须存在,没有则显示错误)
        switch (compare.gameReleaseDate) {
            case '00': rows.gameReleaseDate = `<b>更新时间:</b>${NA_TEXT}<br>`; break;
            case '01': rows.gameReleaseDate = `<b>更新时间:</b>${createColoredSpan(live.gameReleaseDate, COLOR_GREEN)}<br>`; break;
            case '10': rows.gameReleaseDate = `<b>更新时间:</b>${local.gameReleaseDate}<br>`; break;
            case '11': rows.gameReleaseDate = `<b>更新时间:</b>${local.gameReleaseDate}<br>`; break;
            case '99': rows.gameReleaseDate = `<b>更新时间:</b>${createColoredSpan(local.gameReleaseDate, COLOR_RED)} ⇒ ${createColoredSpan(live.gameReleaseDate, COLOR_GREEN)}<br>`; break;
        }
        // 中文
        switch (compare.gameHasChinese) {
            case '00': rows.gameHasChinese = `<b>中  文:</b>×<br>`; break;
            case '01': rows.gameHasChinese = `<b>中  文:</b>${createColoredSpan(live.gameHasChinese ? '○' : '×', COLOR_GREEN)}<br>`; break;
            case '10': rows.gameHasChinese = `<b>中  文:</b>${local.gameHasChinese ? '○' : '×'}<br>`; break;
            case '11': rows.gameHasChinese = `<b>中  文:</b>${local.gameHasChinese ? '○' : '×'}<br>`; break;
            case '99': rows.gameHasChinese = live.gameHasChinese ? `<b>中  文:</b>${createColoredSpan('×', COLOR_RED)} ⇒ ${createColoredSpan('○', COLOR_GREEN)}<br>` : `<b>中  文:</b>${local.gameHasChinese ? '○' : '×'}<br>`; break; // 不可能原来有中文,现在没有
        }
        // 游戏类型(不一定存在,没有则留空)
        switch (compare.gameType) {
            case '00': rows.gameType = `<b>游戏类型:</b><br>`; break;
            case '01': rows.gameType = `<b>游戏类型:</b>${createColoredSpan(live.gameType, COLOR_GREEN)}<br>`; break;
            case '10': rows.gameType = `<b>游戏类型:</b>${local.gameType}<br>`; break;
            case '11': rows.gameType = `<b>游戏类型:</b>${local.gameType}<br>`; break;
            case '99': rows.gameType = `<b>游戏类型:</b>${createColoredSpan(local.gameType, COLOR_RED)} ⇒ ${createColoredSpan(live.gameType, COLOR_GREEN)}<br>`; break;
        }
        // 游戏类型(不一定存在,没有则留空)
        switch (compare.gameEngine) {
            case '00': rows.gameEngine = `<b>游戏引擎:</b><br>`; break;
            case '01': rows.gameEngine = `<b>游戏引擎:</b>${createColoredSpan(live.gameEngine, COLOR_GREEN)}<br>`; break;
            case '10': rows.gameEngine = `<b>游戏引擎:</b>${local.gameEngine}<br>`; break;
            case '11': rows.gameEngine = `<b>游戏引擎:</b>${local.gameEngine}<br>`; break;
            case '99': rows.gameEngine = `<b>游戏引擎:</b>${createColoredSpan(local.gameEngine, COLOR_RED)} ⇒ ${createColoredSpan(live.gameEngine, COLOR_GREEN)}<br>`; break;
        }
        // f95 ID、评分数量、平均评分
        rows.f95ThreadId = `<b>F95 ID:</b>${live.f95ThreadId || local.f95ThreadId}<br>`; // f95ThreadId 理论上不会变
        switch (compare.f95VoteCount) {
            case '00': rows.f95VoteCount = `<b>评分数量:</b>${NA_TEXT}<br>`; break;
            case '01': rows.f95VoteCount = `<b>评分数量:</b>${createColoredSpan(live.f95VoteCount+' 人', COLOR_GREEN)}<br>`; break;
            case '10': rows.f95VoteCount = `<b>评分数量:</b>${local.f95VoteCount} 人<br>`; break;
            case '11': rows.f95VoteCount = `<b>评分数量:</b>${local.f95VoteCount} 人<br>`; break;
            case '99': rows.f95VoteCount = `<b>评分数量:</b>${createColoredSpan(local.f95VoteCount+' 人', COLOR_RED)} ⇒ ${createColoredSpan(live.f95VoteCount+' 人', COLOR_GREEN)}<br>`; break;
        }
        switch (compare.f95AvgScore) {
            case '00': rows.f95AvgScore = `<b>评  分:</b>${NA_TEXT}<br>`; break;
            case '01': rows.f95AvgScore = `<b>评  分:</b>${createColoredSpan(live.f95AvgScore+' 分', COLOR_GREEN)}<br>`; break;
            case '10': rows.f95AvgScore = `<b>评  分:</b>${local.f95AvgScore} 分<br>`; break;
            case '11': rows.f95AvgScore = `<b>评  分:</b>${local.f95AvgScore} 分<br>`; break;
            case '99': rows.f95AvgScore = `<b>评  分:</b>${createColoredSpan(local.f95AvgScore+' 分', COLOR_RED)} ⇒ ${createColoredSpan(live.f95AvgScore+' 分', COLOR_GREEN)}<br>`; break;
        }
        // VNDB 分隔线、ID、评分数量、平均评分
        if (compare.vndbId !== '00') { // 只有存在 ndbId 时才显示整个区块
            rows.line1 = `<div class="tooltip-separator"></div>`;
            switch (compare.vndbId) {
                case '01': rows.vndbId = `<b>VNDB ID:</b>${createColoredSpan(live.vndbId, COLOR_GREEN)}<br>`; break;
                case '10': rows.vndbId = `<b>VNDB ID:</b>${local.vndbId}<br>`; break;
                case '11': rows.vndbId = `<b>VNDB ID:</b>${local.vndbId}<br>`; break;
                case '99': rows.vndbId = `<b>VNDB ID:</b>${createColoredSpan(local.vndbId, COLOR_RED)} ⇒ ${createColoredSpan(live.vndbId, COLOR_GREEN)}<br>`; break;
            }
            switch (compare.vndbVoteCount) {
                case '00': rows.vndbVoteCount = `<b>评分数量:</b>${NA_TEXT}<br>`; break;
                case '01': rows.vndbVoteCount = `<b>评分数量:</b>${createColoredSpan(live.vndbVoteCount+' 人', COLOR_GREEN)}<br>`; break;
                case '10': rows.vndbVoteCount = `<b>评分数量:</b>${local.vndbVoteCount} 人<br>`; break;
                case '11': rows.vndbVoteCount = `<b>评分数量:</b>${local.vndbVoteCount} 人<br>`; break;
                case '99': rows.vndbVoteCount = `<b>评分数量:</b>${createColoredSpan(local.vndbVoteCount+' 人', COLOR_RED)} ⇒ ${createColoredSpan(live.vndbVoteCount+' 人', COLOR_GREEN)}<br>`; break;
            }
            switch (compare.vndbAvgScore) {
                case '00': rows.vndbAvgScore = `<b>评  分:</b>${NA_TEXT}<br>`; break;
                case '01': rows.vndbAvgScore = `<b>评  分:</b>${createColoredSpan(live.vndbAvgScore+' 分', COLOR_GREEN)}<br>`; break;
                case '10': rows.vndbAvgScore = `<b>评  分:</b>${local.vndbAvgScore} 分<br>`; break;
                case '11': rows.vndbAvgScore = `<b>评  分:</b>${local.vndbAvgScore} 分<br>`; break;
                case '99': rows.vndbAvgScore = `<b>评  分:</b>${createColoredSpan(local.vndbAvgScore+' 分', COLOR_RED)} ⇒ ${createColoredSpan(live.vndbAvgScore+' 分', COLOR_GREEN)}<br>`; break;
            }
        }
        // Steam 分隔线、ID、SteamDB 评分数量、SteamDB 平均评分
        if (compare.steamId !== '00') { // 只有存在 ndbId 时才显示整个区块
            rows.line2 = `<div class="tooltip-separator"></div>`;
            switch (compare.steamId) {
                case '01': rows.steamId = `<b>Steam ID:</b>${createColoredSpan(live.steamId, COLOR_GREEN)}<br>`; break;
                case '10': rows.steamId = `<b>Steam ID:</b>${local.steamId}<br>`; break;
                case '11': rows.steamId = `<b>Steam ID:</b>${local.steamId}<br>`; break;
                case '99': rows.steamId = `<b>Steam ID:</b>${createColoredSpan(local.steamId, COLOR_RED)} ⇒ ${createColoredSpan(live.steamId, COLOR_GREEN)}<br>`; break;
            }
            switch (compare.steamVoteCount) {
                case '00': rows.steamVoteCount = `<b>评分数量:</b>${NA_TEXT}<br>`; break;
                case '01': rows.steamVoteCount = `<b>评分数量:</b>${createColoredSpan(live.steamVoteCount+' 人', COLOR_GREEN)}<br>`; break;
                case '10': rows.steamVoteCount = `<b>评分数量:</b>${local.steamVoteCount} 人<br>`; break;
                case '11': rows.steamVoteCount = `<b>评分数量:</b>${local.steamVoteCount} 人<br>`; break;
                case '99': rows.steamVoteCount = `<b>评分数量:</b>${createColoredSpan(local.steamVoteCount+' 人', COLOR_RED)} ⇒ ${createColoredSpan(live.steamVoteCount+' 人', COLOR_GREEN)}<br>`; break;
            }
            switch (compare.steamAvgScore) {
                case '00': rows.steamAvgScore = `<b>评  分:</b>${NA_TEXT}<br>`; break;
                case '01': rows.steamAvgScore = `<b>评  分:</b>${createColoredSpan(live.steamAvgScore+' 分', COLOR_GREEN)}<br>`; break;
                case '10': rows.steamAvgScore = `<b>评  分:</b>${local.steamAvgScore} 分<br>`; break;
                case '11': rows.steamAvgScore = `<b>评  分:</b>${local.steamAvgScore} 分<br>`; break;
                case '99': rows.steamAvgScore = `<b>评  分:</b>${createColoredSpan(local.steamAvgScore+' 分', COLOR_RED)} ⇒ ${createColoredSpan(live.steamAvgScore, COLOR_GREEN)+' 分'}<br>`; break;
            }
        }
        // DLsite RJ
        if (compare.dlsiteUrl !== '00') {
            rows.line3 = `<div class="tooltip-separator"></div>`;
            const dlsiteId1 = extractDlsiteId(local.dlsiteUrl);
            const dlsiteId2 = extractDlsiteId(live.dlsiteUrl);
            switch (compare.dlsiteUrl) {
                case '01': rows.dlsiteUrl = `<b>dlsite RJ:</b>${createColoredSpan(dlsiteId2, COLOR_GREEN)}<br>`; break;
                case '10': rows.dlsiteUrl = `<b>dlsite RJ:</b>${dlsiteId1}<br>`; break;
                case '11': rows.dlsiteUrl = `<b>dlsite RJ:</b>${dlsiteId1}<br>`; break;
                case '99': rows.dlsiteUrl = `<b>dlsite RJ:</b>${createColoredSpan(dlsiteId1, COLOR_RED)} ⇒ ${createColoredSpan(dlsiteId2, COLOR_GREEN)}<br>`; break;
            }
        }

        tooltipElement.innerHTML = [
            rows.gameName1,
            rows.gameName2,
            rows.gameName3,
            rows.gameDev,
            rows.line0,
            rows.gameVersion,
            rows.gameDevStatus,
            rows.gameReleaseDate,
            rows.gameHasChinese,
            rows.gameType,
            rows.gameEngine,
            rows.line0,
            rows.f95ThreadId,
            rows.f95VoteCount,
            rows.f95AvgScore,
            rows.line1,
            rows.vndbId,
            rows.vndbVoteCount,
            rows.vndbAvgScore,
            rows.line2,
            rows.steamId,
            rows.steamVoteCount,
            rows.steamAvgScore,
            rows.line3,
            rows.dlsiteUrl,
        ].filter(Boolean).join(''); // filter(Boolean) 会自动过滤掉所有 undefined 或空字符串的行

        const btnRect = buttonElement.getBoundingClientRect();
        tooltipElement.style.display = 'block';
        const tooltipRect = tooltipElement.getBoundingClientRect();
        tooltipElement.style.top = `${btnRect.top}px`;
        tooltipElement.style.left = `${btnRect.left - tooltipRect.width - 10}px`;
    }

    /**
     * 提取本地存储的游戏信息
     * @param {string} threadId - 游戏的 F95 Thread ID
     * @returns {object|null} 如果找到则返回游戏信息对象,否则返回 null
     */
    function getLocalInfo(threadId) {
        if (!threadId) { return null; }
        const DB_KEY = 'f95GameDatabase';
        try {
            const database = JSON.parse(GM_getValue(DB_KEY, '{}'));
            return database[threadId] || null; // 如果数据库中没有该ID的条目,返回null
        } catch (error) {
            console.error('[F95助手] 从本地存储读取游戏信息时出错:', error);
            return null;
        }
    }

    /**
     * 比较函数:对比本地存储信息和实时页面信息
     * @param {object|null} localInfo - 从 getLocalInfo() 返回的本地数据,或 null
     * @param {object|null} liveInfo - 从 f95GameInfo() 或 vndbGameInfo() 返回的实时数据
    * @returns {{local: object, live: object, compare: object}|null} 包含三个标准化对象的返回结果,或在 liveInfo 无效时返回 null
    */
    function dataCompare(localInfo, liveInfo) {
        if (!liveInfo) {
            console.warn('[F95助手] dataCompare 函数接收到无效的 liveInfo,已中止比较。');
            return null;
        }
        // 数据全集(包含所有可能字段的模板)
        const dataTemplate = {
            f95ThreadId: null,
            gameName1: null,
            gameName1Official: null, // 布尔
            gameName2: null,
            gameName2Official: null, // 布尔
            gameName3: null,
            gameVersion: null,
            gameDevStatus: null,
            gameReleaseDate: null,
            gameDev: null,
            gameEngine: null,
            gameType: null,
            gameHasChinese: null, // 布尔
            gameIsAI: null, // 布尔
            f95VoteCount: null,
            f95AvgScore: null,
            vndbId: null,
            vndbVoteCount: null,
            vndbAvgScore: null,
            steamId: null,
            steamVoteCount: null,
            steamAvgScore: null,
            dlsiteUrl: null,
        };

        const local = { ...dataTemplate, ...(localInfo || {}) };
        const live = { ...dataTemplate, ...liveInfo };
        const compare = {};

        // 3. 遍历模板的所有键 (字段),进行比较
        for (const key in dataTemplate) {
            const localValue = local[key];
            const liveValue = live[key];

            if (localValue === null && liveValue === null) {
                compare[key] = '00'; // 都不存在
            } else if (localValue === null && liveValue !== null) {
                compare[key] = '01'; // 新增
            } else if (localValue !== null && liveValue === null) {
                compare[key] = '10'; // 丢失
            } else if (localValue !== null && liveValue !== null) {
                if (localValue === liveValue) {
                    compare[key] = '11'; // 存在且相同
                } else {
                    compare[key] = '99'; // 存在但不同 (发生变化)
                }
            }
        }

        return { local, live, compare };
    }

    /**
     * 统一的数据更新入口函数。根据来源站点,智能合并信息到本地数据库。
     * @param {object} liveInfo - 从页面实时抓取的信息对象
     * @param {string} site - 来源站点的标识 ('f95', 'vndb', 'steamdb')
     * @param {string} [matchedThreadId=null] - 对于vndb/steamdb,传入已匹配的f95ThreadId
     */
    function updateLocalDatabase(liveInfo, site, matchedThreadId = null) {
        // 内部辅助函数:根据规则应用更新(更新规则 rules 选项为 live / local / force-live / force-local )
        function applyUpdateRules(localInfo, liveInfo, rules = 'live') {
            const { compare } = dataCompare(localInfo, liveInfo);
            if (!compare) return localInfo || {};

            const updatedInfo = { ...(localInfo || {}) };

            for (const key in rules) {
                const rule = rules[key];
                if (!rule || !compare.hasOwnProperty(key)) continue;

                const localValue = localInfo ? localInfo[key] : null;
                const liveValue = liveInfo[key];
                let newValue = updatedInfo[key]; // 默认保持不变

                switch (rule) {
                    // --- 新的规则命名 ---
                    case 'live':       // 优先使用实时数据,但前提是它不为 null
                        newValue = liveValue !== null ? liveValue : localValue;
                        break;
                    case 'local':      // 优先使用本地数据,但前提是它不为 null
                        newValue = localValue !== null ? localValue : liveValue;
                        break;
                    case 'force-live': // 强制使用实时数据,即使它是 null
                        newValue = liveValue;
                        break;
                    case 'force-local':// 强制保留本地数据,即使它是 null
                        newValue = localValue;
                        break;
                }
                updatedInfo[key] = newValue;
            }
            return updatedInfo;
        }

        // 主逻辑
        const DB_KEY = 'f95GameDatabase';
        const threadId = site === 'f95' ? liveInfo.f95ThreadId : matchedThreadId;

        if (!liveInfo || !threadId) {
            console.warn('[F95助手] 因缺少有效信息或ThreadId,已跳过数据库更新。');
            return;
        }

        try {
            const database = JSON.parse(GM_getValue(DB_KEY, '{}'));
            const localInfo = database[threadId] || null;

            let rules = {};
            let siteName = '';

            // 【微调地点】数据更新规则
            switch (site) {
                case 'f95':
                    siteName = 'F95页面';
                    rules = {
                        gameName1: 'live', // 可靠性高
                        gameDev: 'local', // 作者名不要动
                        gameVersion: 'live',
                        gameDevStatus: 'live',
                        gameReleaseDate: 'live',
                        gameHasChinese: 'local', // 可靠性低
                        gameType: 'local', // 可靠性低
                        gameEngine: 'local', // 可靠性低
                        gameIsAI: 'live', // 可靠性高
                        steamId: 'local', // 不会变
                        dlsiteUrl: 'local', // 不会变
                        vndbId: 'local', // 不会变
                        f95VoteCount: 'live',
                        f95AvgScore: 'live',
                    };
                    break;
                case 'vndb':
                    siteName = 'VNDB页面';
                    rules = {
                        gameName1: 'local', // 可靠性低
                        gameName1Official: 'local', // 可靠性低
                        gameName2: 'local', // 可靠性低
                        gameName2Official: 'local', // 可靠性低
                        gameName3: 'local', // 可靠性低
                        gameDev: 'local', // 可靠性低
                        gameHasChinese: 'local', // 可靠性低
                        vndbId: 'live',
                        vndbVoteCount: 'live',
                        vndbAvgScore: 'live',
                    };
                    break;
                case 'steamdb':
                    siteName = 'SteamDB页面';
                    rules = {
                        gameName1: 'live', // 可靠性高
                        gameName1Official: 'live', // 可靠性高
                        gameName2: 'live', // 可靠性高
                        gameName2Official: 'live', // 可靠性高
                        gameHasChinese: 'live', // 可靠性高
                        gameDev: 'live', // 可靠性高
                        gameEngine: 'live', // 可靠性高
                        steamId: 'live',
                        steamVoteCount: 'live',
                        steamAvgScore: 'live',
                    };
                    break;
                default:
                    console.warn(`[F95助手] 未知的站点类型: ${site}`);
                    return;
            }

            const updatedInfo = applyUpdateRules(localInfo, liveInfo, rules);
            updatedInfo.f95ThreadId = threadId; // 确保主键ID始终存在

            database[threadId] = updatedInfo;
            GM_setValue(DB_KEY, JSON.stringify(database));

            const action = localInfo ? '更新' : '新增';
            console.log(`[F95助手] 已使用 ${siteName} ${action}游戏信息: ${updatedInfo.gameName1 || liveInfo.gameName1} (ID: ${threadId})`);

        } catch (error) {
            console.error(`[F95助手] 使用 ${site} 信息更新数据库时出错:`, error);
        }
    }

    // -------------------- F95 --------------------
    // F95按钮(复制信息、Steam 跳转/搜索、VNDB 跳转/搜索)
    function f95Buttons() {
        // 1. 调用核心UI函数创建通用界面
        const { buttonContainer, baseButtonStyle, tooltip } = createButtonUI();
        // 2. 创建f95页面专属的按钮
        // “复制信息”按钮
        const copyButton = document.createElement('button');
        copyButton.textContent = '复制信息';
        copyButton.classList.add('f95-helper-button');
        Object.assign(copyButton.style, baseButtonStyle, { backgroundColor: '#007bff' });
        buttonContainer.appendChild(copyButton);
        // “跳转 SteamDB”按钮
        const steamButton = document.createElement('button');
        steamButton.textContent = '跳转 SteamDB';
        steamButton.classList.add('f95-helper-button');
        Object.assign(steamButton.style, baseButtonStyle, { backgroundColor: '#223D58' });
        buttonContainer.appendChild(steamButton);
        // “跳转 VNDB”按钮
        const vndbButton = document.createElement('button');
        vndbButton.textContent = '跳转 VNDB';
        vndbButton.classList.add('f95-helper-button');
        Object.assign(vndbButton.style, baseButtonStyle, { backgroundColor: '#223D58' });
        buttonContainer.appendChild(vndbButton);

        // 3. 绑定f95页面专属的事件
        // 复制按钮的悬停和点击事件
        copyButton.addEventListener('mouseover', () => buttonTooltip(tooltip, copyButton, 'f95'));
        copyButton.addEventListener('mouseout', () => { tooltip.style.display = 'none'; });
        copyButton.addEventListener('click', () => copyButtonClick(copyButton));
        // SteamDB 跳转/搜索按钮的点击事件
        steamButton.addEventListener('click', () => {
            const info = f95GameInfo();
            if (!info) {
                alert('无法解析页面信息!');
                return;
            }
            const localInfo = getLocalInfo(info.f95ThreadId);
            if (localInfo && localInfo.steamId) { // 如果本地有ID,直接跳转
                // const steamUrl = `https://store.steampowered.com/app/${localInfo.steamId}/_/?l=schinese`;
                const steamUrl = `https://steamdb.info/app/${localInfo.steamId}/info/`;
                window.open(steamUrl, '_blank');
            } else { // 如果本地没有ID,执行搜索
                // steamButton.textContent = '搜索 Steam';
                // steamButton.style.backgroundColor = '#9e6d81';
                // ▲待修改。点击后再变文字太慢了,后面有时间再改成 mouseover 触发
                if (!info.gameName1) {
                    alert('无法获取有效的游戏名进行搜索!');
                    return;
                }
                const searchTerm = encodeURIComponent(info.gameName1);
                // const steamUrl = `https://store.steampowered.com/search/?term=${searchTerm}&supportedlang=schinese%2Cenglish&category1=998%2C21&ndl=1`;
                const steamUrl = `https://steamdb.info/search/?a=all&q=${searchTerm}`;
                window.open(steamUrl, '_blank');
            }
        });
        // VNDB 跳转/搜索按钮的点击事件
        vndbButton.addEventListener('click', () => {
            const info = f95GameInfo();
            if (!info) {
                alert('无法解析页面信息!');
                return;
            }
            const localInfo = getLocalInfo(info.f95ThreadId);
            if (localInfo && localInfo.vndbId) { // 如果本地有ID,直接跳转
                const vndbUrl = `https://vndb.org/v${localInfo.vndbId}`;
                window.open(vndbUrl, '_blank');
            } else { // 如果本地没有ID,执行搜索
                // vndbButton.textContent = '搜索 VNDB';
                // vndbButton.style.backgroundColor = '#9e6d81';
                // ▲待修改。点击后再变文字太慢了,后面有时间再改成 mouseover 触发
                if (!info.gameName1) {
                    alert('无法获取有效的游戏名进行搜索!');
                    return;
                }
                const searchTerm = encodeURIComponent(info.gameName1);
                const vndbUrl = `https://vndb.org/v?sq=${searchTerm}`;
                window.open(vndbUrl, '_blank');
            }
        });

        // 内部辅助函数:处理复制按钮的点击事件逻辑
        function copyButtonClick() {
            const liveInfo = f95GameInfo();
            if (!liveInfo) { alert('错误:无法找到或解析标题!'); return; }
            updateLocalDatabase(liveInfo, 'f95'); // 点击复制按钮后,数据同时会更新到本地存储

            const localInfo = getLocalInfo(liveInfo.f95ThreadId);
            if (!localInfo) { alert('错误:数据导入失败!'); return; }

            let outputString = '';

            const copyButtonOutputStyle = GM_getValue('copyButtonOutputStyle', false);
            if (copyButtonOutputStyle) {
                // 自用样式(“收集癖”):输出Excel的TSV格式
                const excelGameEngine = localInfo.gameEngine || '';
                const excelCgEngine = localInfo.gameIsAI ? 'AI' : ''; // CG软件(如果不是AI,则为空值)
                const excelArtStyle = ''; // 画风(预留)
                const excelGameType1 = localInfo.gameType || ''; // 游戏大类
                const excelGameType2 = ''; // 游戏小类(预留)
                const excelGameName1 = localInfo.gameName1 || '';
                const excelGameName2 = localInfo.gameName2 || '';
                const excelGameDev = localInfo.gameDev || '';
                let excelGameDevStatus = '△';
                if (localInfo.gameDevStatus === '完成') excelGameDevStatus = '●';
                else if (localInfo.gameDevStatus === '弃坑') excelGameDevStatus = '▲';
                const excelGameVersion = localInfo.gameVersion || '';
                const excelGameReleaseDate = localInfo.gameReleaseDate || '';
                let excelHyperlink1 = ''; // f95链接
                if(localInfo.f95ThreadId){
                    const voteInfo = localInfo.f95VoteCount ? `${localInfo.f95VoteCount}人` : 'N/A人';
                    const scoreInfo = (localInfo.f95VoteCount >= 10 && localInfo.f95AvgScore) ? `${localInfo.f95AvgScore}分` : 'N/A分';
                    excelHyperlink1 = `=HYPERLINK("https://f95zone.to/threads/${localInfo.f95ThreadId}/","f95 ${voteInfo} ${scoreInfo}")`;
                }
                let excelHyperlink2 = ''; // 官方链接 Steam/Dlsite/……
                if(localInfo.steamId){
                    const voteInfo = localInfo.steamVoteCount ? `${localInfo.steamVoteCount}人` : 'N/A人';
                    const scoreInfo = (localInfo.steamVoteCount >= 10 && localInfo.steamAvgScore) ? `${localInfo.steamAvgScore}分` : 'N/A分';
                    excelHyperlink2 = `=HYPERLINK("https://store.steampowered.com/app/${localInfo.steamId}/_/?l=schinese","Steam ${voteInfo} ${scoreInfo}")`;
                }
                else if (localInfo.dlsiteUrl) excelHyperlink2 = `=HYPERLINK("${localInfo.dlsiteUrl}","Dlsite")`;
                // else if (localInfo.patreonUrl) excelHyperlink3 = `=HYPERLINK("${localInfo.patreonUrl}","Patreon")`; // (预留)
                let excelHyperlink3 = ''; // 其他链接(预留)
                const excelGameHasChinese = localInfo.gameHasChinese ? '○' : '×';
                let excelHyperlink4 = ''; // vndb链接
                if(localInfo.vndbId){
                    const voteInfo = localInfo.vndbVoteCount ? `${localInfo.vndbVoteCount}人` : 'N/A人';
                    const scoreInfo = (localInfo.vndbVoteCount >= 10 && localInfo.vndbAvgScore) ? `${localInfo.vndbAvgScore}分` : 'N/A分';
                    excelHyperlink4 = `=HYPERLINK("https://vndb.org/v${localInfo.vndbId}/","vndb ${voteInfo} ${scoreInfo}")`;
                }
                const outputData = [
                    excelGameEngine,
                    excelCgEngine,
                    excelArtStyle,
                    excelGameType1,
                    excelGameType2,
                    excelGameName1,
                    excelGameName2,
                    excelGameDev,
                    excelGameDevStatus,
                    excelGameVersion,
                    excelGameReleaseDate,
                    excelHyperlink1,
                    excelHyperlink2,
                    excelHyperlink3,
                    excelGameHasChinese,
                    excelHyperlink4
                ];
                outputString = outputData.join('\t');

            } else {
                // 通用样式(“讲介士”):输出一般的文本格式
                const NA_TEXT = 'N/A';
                const outputLines = [];

                outputLines.push(`英文名称:${localInfo.gameName1 || NA_TEXT}`);
                if (localInfo.gameName2) outputLines.push(`中文名称:${localInfo.gameName2}`);
                if (localInfo.gameName3) outputLines.push(`日文名称:${localInfo.gameName3}`);
                outputLines.push(`作  者:${localInfo.gameDev || NA_TEXT}`);
                outputLines.push('--------------------');
                outputLines.push(`版  本:${localInfo.gameVersion || NA_TEXT}`);
                outputLines.push(`开发进度:${localInfo.gameDevStatus || NA_TEXT}`);
                outputLines.push(`更新时间:${localInfo.gameReleaseDate || NA_TEXT}`);
                outputLines.push(`是否中文:${localInfo.gameHasChinese === null ? '未知' : (localInfo.gameHasChinese ? '○' : '×')}`);
                if (localInfo.gameType) outputLines.push(`游戏类型:${localInfo.gameType}`);
                if (localInfo.gameEngine) outputLines.push(`游戏引擎:${localInfo.gameEngine}`);
                if (localInfo.gameIsAI) outputLines.push(`CG类型:AI 生成`);
                // F95 Info
                if (localInfo.f95ThreadId) {
                    outputLines.push('--------------------');
                    const f95Url = `https://f95zone.to/threads/${localInfo.f95ThreadId}/`;
                    outputLines.push(`F95 链接:${f95Url}`);
                    const scoreInfo = (localInfo.f95VoteCount >= 10 && localInfo.f95AvgScore) ? `${localInfo.f95AvgScore} / 10` : NA_TEXT;
                    outputLines.push(`F95 评分:${scoreInfo} (${localInfo.f95VoteCount || 0}人)`);
                }
                // Steam Info
                if (localInfo.steamId) {
                    outputLines.push('--------------------');
                    const steamUrl = `https://store.steampowered.com/app/${localInfo.steamId}/`;
                    outputLines.push(`Steam 链接:${steamUrl}`);
                    const scoreInfo = (localInfo.steamVoteCount >= 10 && localInfo.steamAvgScore) ? `${localInfo.steamAvgScore} / 10` : NA_TEXT;
                    outputLines.push(`SteamDB 评分:${scoreInfo} (${localInfo.steamVoteCount || 0}人)`);
                }
                // VNDB Info
                if (localInfo.vndbId) {
                    outputLines.push('--------------------');
                    const vndbUrl = `https://vndb.org/v${localInfo.vndbId}`;
                    outputLines.push(`VNDB 链接:${vndbUrl}`);
                    const scoreInfo = (localInfo.vndbVoteCount >= 10 && localInfo.vndbAvgScore) ? `${localInfo.vndbAvgScore} / 10` : NA_TEXT;
                    outputLines.push(`VNDB 评分:${scoreInfo} (${localInfo.vndbVoteCount || 0}人)`);
                }
                // Dlsite Info
                if (localInfo.dlsiteUrl) {
                    outputLines.push('--------------------');
                    outputLines.push(`Dlsite 链接:${localInfo.dlsiteUrl}`);
                }
                
                outputString = outputLines.join('\n');
            }

            navigator.clipboard.writeText(outputString).then(() => {
                const originalText = copyButton.textContent;
                copyButton.textContent = '已复制!';
                copyButton.style.backgroundColor = '#28a745';
                setTimeout(() => {
                    copyButton.textContent = originalText;
                    copyButton.style.backgroundColor = '#007bff';
                }, 1500);
            }).catch(err => {
                console.error('复制失败: ', err);
                alert('复制失败!请检查浏览器权限。');
            });
        }
    }
    /**
     * 提取F95页面的游戏信息
     * @returns {object|null} 包含所有游戏信息的对象,或在找不到关键元素时返回 null
     */
    function f95GameInfo() {
        // 所有输出变量
        let f95ThreadId = null; // F95的帖ID
        let gameName1 = null; // 游戏名1
        let gameVersion = null; // 游戏版本
        let gameDevStatus = '更新中'; // 开发进度
        let gameReleaseDate = null; // 更新日期
        let gameDev = null; // 游戏作者
        let gameEngine = null; // 游戏引擎
        let gameType = null; // 游戏大类
        let gameHasChinese = null; // 是否有中文
        let gameIsAI = null; // 是否为AI CG
        let f95VoteCount = null; // 评分数量
        let f95AvgScore = null; // 平均得分 (10分制)
        let steamId = null; // Steam ID
        let dlsiteUrl = null; // DLsite链接

        // 引擎标签的映射(Collection,WebGL,SiteRip这3个标签不会被录入)
        const ENGINE_MAP = new Map([
            ['Ren\'Py', 'Ren\'Py'],
            ['Unity', 'Unity'],
            ['HTML', 'HTML'],
            ['RPGM', 'RPGMaker'],
            ['QSP', 'QSP'],
            ['Unreal Engine', 'Unreal'],
            ['ADRIFT', 'ADRIFT'],
            ['Wolf RPG', 'WolfRPG'],
            ['Flash', 'Flash'],
            ['Java', 'Java'],
            ['RAGS', 'RAGS'],
            ['Tads', 'Tads'],
            ['Others', '未知']
        ]);
        const TYPE_SET = new Map([
            ['VN', 'VN']
        ]);
        const STATUS_SET = new Map([
            ['Completed', '完成'],
            ['Onhold', '弃坑'],
            ['Abandoned', '弃坑']
        ]);

        // F95的帖ID
        const urlMatch = window.location.href.match(/\.(\d+)\/?$/);
        if (urlMatch) { f95ThreadId = urlMatch[1]; }

        const titleElement = document.querySelector('h1.p-title-value'); // 从标题提取信息
        if (!titleElement) return null;

        // 游戏引擎,游戏大类,开发进度
        titleElement.querySelectorAll('.labelLink span').forEach(span => { // 遍历标题的所有标签并进行分类
            const labelText = span.textContent.trim();
            if (ENGINE_MAP.has(labelText)) { gameEngine = ENGINE_MAP.get(labelText); }
            else if (TYPE_SET.has(labelText)) { gameType = TYPE_SET.get(labelText); }
            else if (STATUS_SET.has(labelText)) { gameDevStatus = STATUS_SET.get(labelText); }
        });

        // 游戏作者,游戏版本,游戏名1
        let coreTitleParts = [];
        titleElement.childNodes.forEach(node => { // 遍历h1元素的所有子节点
            if (node.nodeType === 3 && node.textContent.trim() !== '') { // nodeType为3代表文本节点
                coreTitleParts.push(node.textContent.trim());
            }
        }); // 提取非标签的文本内容
        let remainingText = coreTitleParts.join(' '); // 包含作者和版本的字符串,格式应该为"游戏名 [版本] [作者]"
        const devMatch = remainingText.match(/\[([^\]]+)\]$/);
        if (devMatch) {
            gameDev = devMatch[1].trim();
            remainingText = remainingText.substring(0, devMatch.index).trim();
        }
        const versionMatch = remainingText.match(/\[([^\]]+)\]$/);
        if (versionMatch) {
            gameVersion = versionMatch[1].trim();
            remainingText = remainingText.substring(0, versionMatch.index).trim();
        }
        gameName1 = remainingText;

        // 更新日期,是否有中文,Steam ID,dlsite链接
        const postBody = document.querySelector('.bbWrapper'); // 从正文提取信息
        if (postBody) {
            // 是否有中文
            const bodyText = postBody.innerText;
            const releaseMatch = bodyText.match(/^Release Date:\s*(.*)/m);
            if (releaseMatch) {
                gameReleaseDate = releaseMatch[1].trim();
            }
            const langMatch = bodyText.match(/^Language:\s*(.*)/m);
            if (langMatch && langMatch[1].toLowerCase().includes('chinese')) {
                gameHasChinese = true;
            } else {
                // gameHasChinese = false; // 即便F95上没有中文,也不采信
            }

            // Steam ID(优先提取 widget 的URL,然后全局兜底)
            const widgetElement = postBody.querySelector('iframe[data-s9e-mediaembed="steamstore"]'); // widget
            if (widgetElement) {
                const widgetSrc = widgetElement.getAttribute('data-s9e-mediaembed-src');
                if (widgetSrc) {
                    const widgetMatch = widgetSrc.match(/\/widget\/(\d+)/);
                    if (widgetMatch && widgetMatch[1]) {
                        steamId = widgetMatch[1];
                    }
                }
            }
            if (!steamId) {
                const steamLinkElement = postBody.querySelector('a[href*="store.steampowered.com/app/"]');
                if (steamLinkElement) {
                    const steamUrl = steamLinkElement.href;
                    const steamIdMatch = steamUrl.match(/\/app\/(\d+)/);
                    if (steamIdMatch && steamIdMatch[1]) {
                        steamId = steamIdMatch[1];
                    }
                }
            }

            const dlsiteLinkElement = postBody.querySelector('a[href*="/work/=/product_id/"]');
            if (dlsiteLinkElement) {
                let originalUrl = dlsiteLinkElement.href;
                try {
                    // 使用 URL API 进行安全、可靠的格式化
                    const url = new URL(originalUrl);
                    if (url.pathname.endsWith('/')) { url.pathname = url.pathname.slice(0, -1); } // 移除路径末尾可能存在的斜杠
                    url.searchParams.set('locale', 'zh-CN'); // 强制设置 locale 为 zh-CN
                    dlsiteUrl = url.href;
                } catch (error) {
                    console.error('[F95助手] DLsite URL 格式化失败:', error);
                    dlsiteUrl = originalUrl; // 如果解析失败,则回退到原始URL
                }
            }
        }

        // 是否为AI CG
        $('span.js-tagList a').each(function() {
            if ($(this).text().toLowerCase().trim() === 'ai cg') {
                gameIsAI = true;
                return false;
            }
        }); // 标签列表提取AI信息

        // F95评分数量、F95平均得分
        const pageActionContainer = document.querySelector('.p-title-pageAction');
        if (pageActionContainer) {
            // 投票后的HTML结构
            const ratingStarsRow = pageActionContainer.querySelector('.ratingStarsRow');
            if (ratingStarsRow) {
                const ratingStars = ratingStarsRow.querySelector('.ratingStars[title]');
                if (ratingStars) {
                    const titleMatch = ratingStars.title.match(/(\d+\.?\d*)/);
                    if (titleMatch) {
                        f95AvgScore = parseFloat((parseFloat(titleMatch[1]) * 2).toFixed(1)); // 5分制,需乘以2
                    }
                }
                const voteElement = ratingStarsRow.querySelector('.ratingStarsRow-text div');
                if (voteElement) {
                    const voteMatch = voteElement.textContent.replace(/,/g, '').match(/(\d+)/);
                    if (voteMatch) {
                        f95VoteCount = parseInt(voteMatch[1], 10);
                    }
                }
            }
            // 投票前的HTML结构
            else {
                const ratingWidget = pageActionContainer.querySelector('.br-widget.bratr-rating');
                if (ratingWidget) {
                    const voteElement = ratingWidget.querySelector('.bratr-vote-content div');
                    if (voteElement) {
                        const voteMatch = voteElement.textContent.replace(/,/g, '').match(/(\d+)/);
                        if (voteMatch) {
                            f95VoteCount = parseInt(voteMatch[1], 10);
                        }
                    }
                    let integerPart = 0;
                    let fractionalPart = 0;
                    integerPart = ratingWidget.querySelectorAll('a.br-selected').length;
                    const fractionalStar = ratingWidget.querySelector('a[class*="br-fractional-"]');
                    if (fractionalStar) {
                        const fractionalMatch = fractionalStar.className.match(/br-fractional-(\d+)/);
                        if (fractionalMatch) {
                            fractionalPart = parseInt(fractionalMatch[1], 10) / 100;
                        }
                    }
                    f95AvgScore = parseFloat(((integerPart + fractionalPart) * 2).toFixed(1));
                }
            }
        }

        // 返回所有解析后的信息
        return {
            f95ThreadId, gameEngine, gameType, gameName1, gameDev, gameVersion, gameReleaseDate, gameHasChinese, gameIsAI, gameDevStatus, f95VoteCount, f95AvgScore, steamId, dlsiteUrl
        };
    }


    // -------------------- VNDB --------------------
    // VNDB按钮(补充信息、Steam 跳转/搜索、F95 跳转)
    function vndbButtons() {
        // 1. 调用核心UI函数创建通用界面
        const { buttonContainer, baseButtonStyle, tooltip } = createButtonUI();

        // 2. 创建VNDB页面专属的按钮
        // "补充信息" 按钮
        const supplementButton = document.createElement('button');
        supplementButton.textContent = '补充信息';
        supplementButton.classList.add('f95-helper-button');
        Object.assign(supplementButton.style, baseButtonStyle, { backgroundColor: '#007bff' });
        buttonContainer.appendChild(supplementButton);

        // "跳转 SteamDB" 按钮
        const steamButton = document.createElement('button');
        steamButton.textContent = '跳转 SteamDB';
        steamButton.classList.add('f95-helper-button');
        Object.assign(steamButton.style, baseButtonStyle, { backgroundColor: '#223D58' });
        buttonContainer.appendChild(steamButton);

        // "跳转 F95" 按钮
        const f95Button = document.createElement('button');
        f95Button.textContent = '跳转 F95';
        f95Button.classList.add('f95-helper-button');
        Object.assign(f95Button.style, baseButtonStyle, { backgroundColor: '#223D58' }); // 使用一个不同的颜色以区分
        buttonContainer.appendChild(f95Button);

        // 3. 绑定VNDB页面专属的事件
        // 补充信息按钮的悬停和点击事件
        supplementButton.addEventListener('mouseover', () => buttonTooltip(tooltip, supplementButton, 'vndb'));
        supplementButton.addEventListener('mouseout', () => { tooltip.style.display = 'none'; });
        supplementButton.addEventListener('click', () => {
            const liveInfo = vndbGameInfo();
            if (!liveInfo) {
                alert('错误:无法解析当前VNDB页面信息!');
                return;
            }

            let matchedF95ThreadId = vndbMatchDB(liveInfo);
            if (!matchedF95ThreadId) { // 如果自动匹配失败,则尝试手动输入
                matchedF95ThreadId = promptForF95Id();
                if (!matchedF95ThreadId) { // 如果用户取消或输入无效
                    alert('未提供有效的 F95 ID,操作已取消。');
                    return;
                }
            }

            updateLocalDatabase(liveInfo, 'vndb', matchedF95ThreadId);

            // 提供UI反馈
            const originalText = supplementButton.textContent;
            supplementButton.textContent = '已补充!';
            supplementButton.style.backgroundColor = '#28a745'; // 绿色
            setTimeout(() => {
                supplementButton.textContent = originalText;
                supplementButton.style.backgroundColor = '#007bff'; // 恢复蓝色
            }, 1500);
        });

        // SteamDB 跳转/搜索按钮的点击事件
        steamButton.addEventListener('click', () => {
            const liveInfo = vndbGameInfo();
            if (!liveInfo) {
                alert('无法解析页面信息!');
                return;
            }
            const matchedF95ThreadId = vndbMatchDB(liveInfo);
            const localInfo = getLocalInfo(matchedF95ThreadId);

            if (localInfo && localInfo.steamId) { // 如果本地有ID,直接跳转
                // const steamUrl = `https://store.steampowered.com/app/${localInfo.steamId}/_/?l=schinese`;
                const steamUrl = `https://steamdb.info/app/${localInfo.steamId}/info/`;
                window.open(steamUrl, '_blank');
            } else { // 如果本地没有ID,执行搜索
                if (!liveInfo.gameName1) {
                    alert('无法获取有效的游戏名进行搜索!');
                    return;
                }
                const searchTerm = encodeURIComponent(liveInfo.gameName1);
                // const steamUrl = `https://store.steampowered.com/search/?term=${searchTerm}&supportedlang=schinese%2Cenglish&category1=998%2C21&ndl=1`;
                const steamUrl = `https://steamdb.info/search/?a=all&q=${searchTerm}`;
                window.open(steamUrl, '_blank');
            }
        });

        // F95 跳转/搜索按钮的点击事件
        f95Button.addEventListener('click', () => {
            const liveInfo = vndbGameInfo();
            if (!liveInfo) {
                alert('无法解析页面信息!');
                return;
            }
            const matchedF95ThreadId = vndbMatchDB(liveInfo);

            if (matchedF95ThreadId) { // 如果匹配到ID,直接跳转
                const f95Url = `https://f95zone.to/threads/${matchedF95ThreadId}/`;
                window.open(f95Url, '_blank');
            } else {
                // ▲待修改。如果没有匹配到ID,执行搜索。不过F95搜索逻辑较为复杂,暂不实现。(通过url搜索需要 xfToken ;通过模拟输入需要跨域。)
                alert('未在本地数据库中找到匹配的F95页面。');
            }
        });
    }
    /**
     * 提取 VNDB 页面的游戏信息
     * @returns {object|null} 包含所有游戏信息的对象,或在找不到关键元素时返回 null
     */
    function vndbGameInfo() {
        let vndbId = null;
        let gameName1 = null; // 英文名
        let gameName1Official = null; // 英文名官方标记
        let gameName2 = null; // 中文名
        let gameName2Official = null; // 中文名官方标记
        let gameName3 = null; // 官方日文名
        let gameDev = null; // 游戏作者
        let vndbVoteCount = null; // 评分数量
        let vndbAvgScore = null; // 平均得分 (10分制)
        let gameHasChinese = null; // 游戏是否存在官方中文

        // VNDB ID
        const urlMatch = window.location.pathname.match(/\/v(\d+)/);
        if (urlMatch && urlMatch[1]) {
            vndbId = urlMatch[1];
        }

        // 游戏作者
        document.querySelectorAll('.vndetails > .stripe tr').forEach(row => {
            const cells = row.querySelectorAll('td');
            if (cells.length >= 2 && cells[0].textContent.trim() === 'Developer') { // 确保行中有至少两个单元格,并且第一个单元格的文本是 "Developer"
                const devLink = cells[1].querySelector('a'); // 开发者名称在第二个单元格的 <a> 标签内
                if (devLink) {
                    gameDev = devLink.textContent.trim();
                }
            }
        });

        // 评分数量、平均得分
        const voteStatsFooter = document.querySelector('#stats .votegraph tfoot');
        if (voteStatsFooter) {
            const statsText = voteStatsFooter.textContent;
            // 评分数量
            const voteMatch = statsText.match(/(\d+)\s+votes/);
            if (voteMatch && voteMatch[1]) {
                vndbVoteCount = parseInt(voteMatch[1], 10);
            }
            // 平均得分
            const scoreMatch = statsText.match(/(\d+\.\d+)\s+average/);
            if (scoreMatch && scoreMatch[1]) {
                vndbAvgScore = parseFloat(scoreMatch[1]).toFixed(1);
            }
        }

        // 3种语言的游戏名
        document.querySelectorAll('.vnreleases > details').forEach(detailsElement => {
            const summary = detailsElement.querySelector('summary');
            if (!summary) return;
            const langIcon = summary.querySelector('abbr[class*="icon-lang-"]');
            if (!langIcon) return;
            const language = langIcon.title;
            // 英文游戏名
            if (language === 'English') {
                const firstReleaseRow = detailsElement.querySelector('table.releases tr');
                if (firstReleaseRow) {
                    const titleLink = firstReleaseRow.querySelector('td.tc4 a');
                    if (titleLink) {
                        gameName1 = titleLink.textContent.trim();
                        const unofficialMarker = titleLink.nextElementSibling;
                        gameName1Official = !(unofficialMarker && unofficialMarker.tagName === 'SMALL' && unofficialMarker.textContent.toLowerCase().includes('unofficial'));
                    }
                }
            }
            // 中文游戏名
            if (language === 'Chinese (simplified)') {
                const firstReleaseRow = detailsElement.querySelector('table.releases tr');
                if (firstReleaseRow) {
                    const titleLink = firstReleaseRow.querySelector('td.tc4 a');
                    if (titleLink) {
                        gameName2 = titleLink.textContent.trim();
                        const unofficialMarker = titleLink.nextElementSibling;
                        gameName2Official = !(unofficialMarker && unofficialMarker.tagName === 'SMALL' && unofficialMarker.textContent.toLowerCase().includes('unofficial'));
                        if (gameName2) gameHasChinese = gameName2Official;
                    }
                }
            }
            // 日文官方游戏名
            if (language === 'Japanese') {
                const firstReleaseRow = detailsElement.querySelector('table.releases tr');
                if (firstReleaseRow) {
                    const titleLink = firstReleaseRow.querySelector('td.tc4 a');
                    if (titleLink) {
                        const unofficialMarker = titleLink.nextElementSibling;
                        const isUnofficial = unofficialMarker && unofficialMarker.tagName === 'SMALL' && unofficialMarker.textContent.toLowerCase().includes('unofficial'); // 非官方名判定
                        if (!isUnofficial) {
                            gameName3 = titleLink.textContent.trim();
                        }
                    }
                }
            }
        });

        return {
            vndbId, gameName1, gameName1Official, gameName2, gameName2Official, gameName3, gameDev, vndbVoteCount, vndbAvgScore, gameHasChinese
        };
    }
    /**
     * 从本地数据库中匹配 VNDB 页面的游戏信息,返回 f95ThreadId
     * @param {object} liveInfo - 从 vndbGameInfo() 返回的实时 VNDB 信息对象
     * @returns {string|null} 如果找到匹配的游戏,返回其 f95ThreadId,否则返回 null
     */
    function vndbMatchDB(liveInfo) {
        if (!liveInfo || !liveInfo.vndbId) {
            console.warn('[F95助手] VNDB 页面缺少有效 ID,已跳过比对。');
            return null;
        }
        const database = JSON.parse(GM_getValue('f95GameDatabase', '{}'));
        if (!database) return null;

        // VNDB ID 直接匹配
        for (const threadId in database) {
            if (database[threadId].vndbId === liveInfo.vndbId) {
                return threadId;
            }
        }

        // ID 匹配失败时,进行游戏名称和开发者匹配
        const normLiveName1 = normalizeName(liveInfo.gameName1);
        const normLiveName2 = normalizeName(liveInfo.gameName2);
        const normLiveName3 = normalizeName(liveInfo.gameName3);
        const normVndbDev = normalizeAuthor(liveInfo.gameDev);
        if (!normVndbDev || !(normLiveName1 || normLiveName2 || normLiveName3)) return null;

        for (const threadId in database) {
            const localInfo = database[threadId];
            const normLocalName1 = normalizeName(localInfo.gameName1);
            const normLocalName2 = normalizeName(localInfo.gameName2);
            const normLocalName3 = normalizeName(localInfo.gameName3);
            const normLocalDev = normalizeAuthor(localInfo.gameDev);
            const isDevMatch = normLocalDev === normVndbDev;
            const isTitleMatch = normLiveName1.includes(normLocalName1) || normLiveName2.includes(normLocalName2) || normLiveName3.includes(normLocalName3);
            if (isDevMatch && isTitleMatch) {
                return threadId;
            }
        }

        return null; // 遍历结束仍未找到匹配项
    }


    // -------------------- SteamDB --------------------
    // SteamDB按钮(补充信息、VNDB 跳转/搜索、F95 跳转)
    function steamdbButtons() {
        // 1. 调用核心UI函数创建通用界面
        const { buttonContainer, baseButtonStyle, tooltip } = createButtonUI();

        // 2. 创建SteamDB页面专属的按钮
        // "补充信息" 按钮
        const supplementButton = document.createElement('button');
        supplementButton.textContent = '补充信息';
        supplementButton.classList.add('f95-helper-button');
        Object.assign(supplementButton.style, baseButtonStyle, { backgroundColor: '#007bff' });
        buttonContainer.appendChild(supplementButton);

        // "跳转 VNDB" 按钮
        const vndbButton = document.createElement('button');
        vndbButton.textContent = '跳转 VNDB';
        vndbButton.classList.add('f95-helper-button');
        Object.assign(vndbButton.style, baseButtonStyle, { backgroundColor: '#223D58' });
        buttonContainer.appendChild(vndbButton);

        // "跳转 F95" 按钮
        const f95Button = document.createElement('button');
        f95Button.textContent = '跳转 F95';
        f95Button.classList.add('f95-helper-button');
        Object.assign(f95Button.style, baseButtonStyle, { backgroundColor: '#223D58' });
        buttonContainer.appendChild(f95Button);

        // 3. 绑定SteamDB页面专属的事件
        // 补充信息按钮的悬停和点击事件
        supplementButton.addEventListener('mouseover', () => buttonTooltip(tooltip, supplementButton, 'steamdb'));
        supplementButton.addEventListener('mouseout', () => { tooltip.style.display = 'none'; });
        supplementButton.addEventListener('click', () => {
            const liveInfo = steamdbGameInfo();
            if (!liveInfo) {
                alert('错误:无法解析当前SteamDB页面信息!');
                return;
            }

            let matchedF95ThreadId = steamdbMatchDB(liveInfo);
            if (!matchedF95ThreadId) { // 如果自动匹配失败,则尝试手动输入
                matchedF95ThreadId = promptForF95Id();
                if (!matchedF95ThreadId) { // 如果用户取消或输入无效
                    alert('未提供有效的 F95 ID,操作已取消。');
                    return;
                }
            }

            updateLocalDatabase(liveInfo, 'steamdb', matchedF95ThreadId);

            // 提供UI反馈
            const originalText = supplementButton.textContent;
            supplementButton.textContent = '已补充!';
            supplementButton.style.backgroundColor = '#28a745'; // 绿色
            setTimeout(() => {
                supplementButton.textContent = originalText;
                supplementButton.style.backgroundColor = '#007bff'; // 恢复蓝色
            }, 1500);
        });

        // VNDB 跳转/搜索按钮的点击事件
        vndbButton.addEventListener('click', () => {
            const liveInfo = steamdbGameInfo();
            if (!liveInfo) {
                alert('无法解析页面信息!');
                return;
            }
            const matchedF95ThreadId = steamdbMatchDB(liveInfo);
            const localInfo = getLocalInfo(matchedF95ThreadId);

            if (localInfo && localInfo.vndbId) { // 如果本地有ID,直接跳转
                const vndbUrl = `https://vndb.org/v${localInfo.vndbId}`;
                window.open(vndbUrl, '_blank');
            } else { // 如果本地没有ID,执行搜索
                if (!liveInfo.gameName1) { // 使用从SteamDB获取的游戏名
                    alert('无法获取有效的游戏名进行搜索!');
                    return;
                }
                const searchTerm = encodeURIComponent(liveInfo.gameName1);
                const vndbUrl = `https://vndb.org/v?sq=${searchTerm}`;
                window.open(vndbUrl, '_blank');
            }
        });

        // F95 跳转/搜索按钮的点击事件
        f95Button.addEventListener('click', () => {
            const liveInfo = steamdbGameInfo();
            if (!liveInfo) {
                alert('无法解析页面信息!');
                return;
            }
            const matchedF95ThreadId = steamdbMatchDB(liveInfo);

            if (matchedF95ThreadId) { // 如果匹配到ID,直接跳转
                const f95Url = `https://f95zone.to/threads/${matchedF95ThreadId}/`;
                window.open(f95Url, '_blank');
            } else {
                // ▲待修改。如果没有匹配到ID,执行搜索。不过F95搜索逻辑较为复杂,暂不实现。(通过url搜索需要 xfToken ;通过模拟输入需要跨域。)
                alert('未在本地数据库中找到匹配的F95页面。');
            }
        });
    }

    /**
     * 提取 SteamDB 页面的游戏信息
     * @returns {object|null} 包含所有游戏信息的对象,或在找不到关键元素时返回 null
     */
    function steamdbGameInfo() {
        let steamId = null;
        let gameName1 = null; // 英文名
        let gameName1Official = null; // 英文名官方标记
        let gameName2 = null; // 中文名
        let gameName2Official = null; // 中文名官方标记
        let gameDev = null; // 游戏作者
        let gameEngine = null; // 游戏引擎
        let steamVoteCount = null; // 评分数量
        let steamAvgScore = null; // 平均得分 (10分制)
        let gameHasChinese = null; // 游戏是否存在官方中文

        // Steam ID
        const urlMatch = window.location.pathname.match(/\/app\/(\d+)/);
        if (urlMatch && urlMatch[1]) {
            steamId = urlMatch[1];
        }
        // 英文游戏名
        const nameElement = document.querySelector('h1[itemprop="name"]');
        if (nameElement) {
            gameName1 = nameElement.textContent.trim();
        }
        // 游戏引擎
        document.querySelectorAll('.table.table-bordered.table-responsive-flex tbody tr').forEach(row => { // 遍历主信息表来查找"Technologies"行
            const cells = row.querySelectorAll('td');
            if (cells.length >= 2 && cells[0].textContent.trim() === 'Technologies') {
                gameEngine = cells[1].textContent.trim();
                // 简化常见的引擎名称,与F95的习惯保持一致
                if (gameEngine.includes('Unity')) gameEngine = 'Unity';
                else if (gameEngine.includes('Unreal')) gameEngine = 'Unreal';
                else if (gameEngine.includes("RenPy Engine")) gameEngine = "Ren'Py";
                else if (gameEngine.includes("RPGMaker Engine")) gameEngine = "RPGMaker";
            }
        });
        // 非官方游戏名标记 (根据是否有字幕支持来判断是否为官方名称)
        let hasEnglishSubtitles = false;
        let hasChineseSubtitles = false;
        const languagesHeading = Array.from(document.querySelectorAll('h2')).find(h2 => h2.textContent.trim() === 'Supported Languages');
        if (languagesHeading) {
            const languagesTable = languagesHeading.nextElementSibling;
            if (languagesTable && languagesTable.tagName === 'TABLE') {
                languagesTable.querySelectorAll('tbody tr').forEach(row => {
                    const cells = row.querySelectorAll('td');
                    if (cells.length >= 4) { // 确保有足够多的列来获取字幕信息
                        const langName = cells[0].textContent.trim();
                        const subtitlesSupport = cells[3].textContent.trim() === 'Yes';
                        if (langName === 'English') {
                            hasEnglishSubtitles = subtitlesSupport;
                        } else if (langName === 'Simplified Chinese') {
                            hasChineseSubtitles = subtitlesSupport;
                        }
                    }
                });
            }
        }
        gameName1Official = hasEnglishSubtitles;
        gameName2Official = hasChineseSubtitles;
        gameHasChinese = hasChineseSubtitles;
        // 开发者, 中文游戏名
        const infoTable = document.querySelector('#info .table.table-bordered');
        if (infoTable) {
            infoTable.querySelectorAll('tbody > tr').forEach(row => {
                const cells = row.querySelectorAll('td');
                if (cells.length < 2) return;
                const key = cells[0].textContent.trim();
                const valueCell = cells[1];
                if (key === 'Developer') {
                    gameDev = valueCell.textContent.trim();
                } else if (key === 'name_localized') { // 在内嵌的本地化名称表格中查找中文名
                    const localizedTable = valueCell.querySelector('table');
                    if (localizedTable) {
                        localizedTable.querySelectorAll('tbody tr').forEach(langRow => {
                            const langCells = langRow.querySelectorAll('td');
                            if (langCells.length >= 2 && langCells[0].textContent.trim() === 'schinese') {
                                gameName2 = langCells[1].textContent.trim();
                            }
                        });
                    }
                }
            });
        }
        // 评分和数量
        const reviewsElement = document.querySelector('#js-reviews-button');
        if (reviewsElement) {
            const ariaLabel = reviewsElement.getAttribute('aria-label');
            if (ariaLabel) {
                const scoreMatch = ariaLabel.match(/^(\d+\.?\d*)%/);
                if (scoreMatch && scoreMatch[1]) {
                    steamAvgScore = parseFloat((parseFloat(scoreMatch[1]) / 10).toFixed(1)); // 将百分制评分转换为10分制,并保留一位小数
                }
            }
            const voteMeta = reviewsElement.querySelector('meta[itemprop="reviewCount"]');
            if (voteMeta) { // 由于html结构可能不一致,需要变换获取 steamVoteCount 的位置
                steamVoteCount = parseInt(voteMeta.getAttribute('content'), 10);
            }
            else if (ariaLabel) {
                const voteMatch = ariaLabel.match(/of the ([\d,]+) user reviews/);
                if (voteMatch && voteMatch[1]) {
                    steamVoteCount = parseInt(voteMatch[1].replace(/,/g, ''), 10);
                }
            }
        }

        const result = {
            steamId,
            gameName1,
            gameName1Official,
            gameName2,
            gameName2Official,
            gameDev,
            gameEngine,
            steamVoteCount,
            steamAvgScore,
            gameHasChinese,
        };
        console.log('[F95助手] 提取的SteamDB信息:', result);
     
        return result;
    }
    /**
     * 从本地数据库中匹配 SteamDB 页面的游戏信息,返回 f95ThreadId
     * @param {object} liveInfo - 从 steamdbGameInfo() 返回的实时 SteamDB 信息对象
     * @returns {string|null} 如果找到匹配的游戏,返回其 f95ThreadId,否则返回 null
     */
    function steamdbMatchDB(liveInfo) {
        if (!liveInfo || !liveInfo.steamId) {
            console.warn('[F95助手] SteamDB 页面缺少有效 ID,已跳过比对。');
            return null;
        }
        const database = JSON.parse(GM_getValue('f95GameDatabase', '{}'));
        if (!database) return null;

        // 策略1:Steam ID 直接匹配 (最高优先级)
        for (const threadId in database) {
            if (database[threadId].steamId === liveInfo.steamId) {
                console.log(`[F95助手] 通过Steam ID找到匹配项: ${threadId}`);
                return threadId;
            }
        }

        // 策略2:ID 匹配失败时,进行游戏名称和开发者匹配 (备用策略)
        // 注意:此策略需要 steamdbGameInfo 能够成功提取到 gameName1 和 gameDev
        const normLiveName1 = normalizeName(liveInfo.gameName1);
        const normLiveDev = normalizeAuthor(liveInfo.gameDev);
        if (!normLiveDev || !normLiveName1) {
            console.log('[F95助手] 无法进行名称/作者匹配,信息不足。');
            return null;
        }

        for (const threadId in database) {
            const localInfo = database[threadId];
            const normLocalName1 = normalizeName(localInfo.gameName1);
            const normLocalDev = normalizeAuthor(localInfo.gameDev);

            // 匹配条件:作者名相同,且游戏名互相包含(或基本相同)
            const isDevMatch = normLocalDev === normLiveDev;
            const isTitleMatch = normLiveName1.includes(normLocalName1) || normLocalName1.includes(normLiveName1);
            if (isDevMatch && isTitleMatch) {
                console.log(`[F95助手] 通过名称和作者找到模糊匹配项: ${threadId}`);
                return threadId;
            }
        }

        console.log('[F95助手] 未在数据库中找到任何匹配项。');
        return null; // 遍历结束仍未找到匹配项
    }


    // -------------------- 辅助函数 --------------------
    /**
     * 点击补充信息按钮时,当自动匹配失败(找不到matchedF95ThreadId),通过弹窗询问用户手动输入 F95 的 ID
     * @returns {string|null} 如果用户输入了有效的纯数字ID,则返回该ID字符串,否则返回 null
     */
    function promptForF95Id() {
        const message = "未在本地数据库中找到匹配的游戏。\n\n如果您知道此游戏在 F95zone 上的帖子 ID,请输入它以手动关联(留空或取消则跳过):";
        const input = prompt(message);

        if (!input) { // 处理用户点击“取消”或输入为空字符串的情况
            return null;
        }

        const trimmedId = input.trim();
        if (/^\d+$/.test(trimmedId)) {
            console.log(`[F95助手] 用户手动输入ID: ${trimmedId}`);
            return trimmedId; // 验证成功,返回纯数字ID
        } else {
            alert('输入的ID格式无效,必须是纯数字。');
            return null; // 验证失败
        }
    }
    /**
     * 检查字符串是否包含关键词列表中的任何一个关键词(关键词由英文逗号分隔,忽略大小写)
     * @param {string} text - 被检查的源字符串 (例如: 'Male Protagonist')。
     * @param {string} keywords - 关键词列表 (例如: 'male protag, animated')。
     * @returns {boolean} 如果 text 包含了任何一个关键词,则返回 true,否则返回 false。
     */
    function includesAnyIgnoreCase(text, keywords) {
        if (!keywords) return false;
        const textLower = text.toLowerCase();
        return keywords.split(',').some(kw => textLower.includes(kw.trim().toLowerCase()));
    }
    // 使指定的 HTML 元素可以通过一个“把手”元素进行拖拽
    function makeDraggable(elmnt, handle) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        handle.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
            elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }
    /**
     * 从完整的DLsite URL中提取产品ID (例如 RJ123456)
     * @param {string} url - 完整的DLsite URL
     * @returns {string} 如果成功提取则返回产品ID,否则返回 null
     */
    function extractDlsiteId(url) {
        if (!url) {
            return null;
        }
        const match = url.match(/product_id\/([A-Z]{2}\d+)/); // 正则表达式查找 "product_id/" 后面的两位大写字母+数字的组合
        return match ? match[1] : null;
    }
    /**
     * 游戏标题标准化
     * @param {string} title - 原始标题
     * @returns {string} 清洗和简化后的标题
     * 例如, "A Game: The New Chapter [v2.0 Final]" 会变成 "a game the new chapter"。
     */
    function normalizeName(name) {
        if (!name) return '';
        let norm = name
            .toLowerCase() // 全部转为小写,这是所有比较的基础
            .replace(/\s*\[.*?\]/g, '') // 移除所有方括号及其内容 (例如 [v1.0], [Completed])
            .replace(/\s*\(.*?\)/g, '') // 移除所有圆括号及其内容 (例如 (Full Release))
            .replace(/[-\s]\s*(season|part|chapter|episode|book)\s*(\d+|final)/g, '') // 移除类似 season 1、- season final 之类的后缀
            .replace(/-\s*(final|complete|demo|test)/g, '') // 移除类似 - final 之类的后缀
            .replace(/\s+(final|demo)/g, '') // 移除类似 final 之类的后缀(这一步需要小心,最好不要随意添加)
            .replace(/[^\p{L}\p{N}]/gu, '') // 只保留文字(Letter)、数字(Number)
            .trim();
        return norm;
    }
    /**
     * 作者名字标准化
     * @param {string} author - 原始作者名
     * @returns {string} 清洗和简化后的名字
     */
    function normalizeAuthor(author) {
        if (!author) return '';
        return author.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
    }
    // 当前URL是否包含传递的字符串
    function isURL(x) {
        return window.location.href.indexOf(x) != -1;
    }
})();