B站网页版一键开播/关播 mod

在B站直播姬网页版添加按钮,动态获取RoomID和分区,选择后一键开播/关播,并用HTML展示结果及复制按钮(成功信息手动关闭)。已加入签名逻辑以避免风控。新增身份验证二维码展示功能。

// ==UserScript==
// @name         B站网页版一键开播/关播 mod
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  在B站直播姬网页版添加按钮,动态获取RoomID和分区,选择后一键开播/关播,并用HTML展示结果及复制按钮(成功信息手动关闭)。已加入签名逻辑以避免风控。新增身份验证二维码展示功能。
// @author       Owwk
// @match        https://link.bilibili.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      api.live.bilibili.com
// @connect      api.qrserver.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let currentRoomInfo = null;
    let availableAreas = null;
    let csrfTokenCache = null;
    let dedeUserIDCache = null; // 新增:缓存 DedeUserID
    let resultBoxTimeoutId = null; // 用于存储结果显示框的定时器ID

    // 1. 函数:从 cookie 中获取 CSRF token (bili_jct)
    function getCsrfToken() {
        if (csrfTokenCache) return csrfTokenCache;
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            let cookie = cookies[i].trim();
            if (cookie.startsWith('bili_jct=')) {
                csrfTokenCache = cookie.substring('bili_jct='.length);
                return csrfTokenCache;
            }
        }
        return null;
    }

    // 新增:从 cookie 中获取 DedeUserID
    function getDedeUserID() {
        if (dedeUserIDCache) return dedeUserIDCache;
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            let cookie = cookies[i].trim();
            if (cookie.startsWith('DedeUserID=')) {
                dedeUserIDCache = cookie.substring('DedeUserID='.length);
                return dedeUserIDCache;
            }
        }
        return null;
    }

    // 辅助函数:发送API请求
    function makeApiRequest(options) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                ...options,
                headers: {
                    'Content-Type': options.method === 'POST' ? 'application/x-www-form-urlencoded; charset=UTF-8' : undefined,
                    'Referer': 'https://live.bilibili.com/p/html/web-hime/index.html',
                    'Origin': 'https://live.bilibili.com',
                    ...(options.headers || {})
                },
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        // 对于关播重复请求,B站API会返回 code = 160000 (或 message "重复关播") 但实际上可能是成功关播或未直播状态,这里视为非严重错误。
                        if (data.code === 0 || (options.url.includes('stopLive') && (data.code === 160000 || data.msg === "重复关播"))) {
                            resolve(data);
                        } else {
                            // 对于需要身份验证的错误码 60024,也需要特殊处理
                            if (data.code === 60024) {
                                reject(new Error(`API Error (${options.url}): ${data.code} - 需要进行身份验证`));
                            } else {
                                reject(new Error(`API Error (${options.url}): ${data.code} - ${data.message || data.msg || '未知API错误'}`));
                            }
                        }
                    } catch (e) {
                        console.error("原始错误响应:", response.responseText);
                        reject(new Error(`JSON解析错误 (${options.url}): ${e.message}`));
                    }
                },
                onerror: function(error) {
                    reject(new Error(`请求错误 (${options.url}): ${JSON.stringify(error)}`));
                },
                ontimeout: function() {
                    reject(new Error(`请求超时 (${options.url})`));
                }
            });
        });
    }

    async function fetchLatestLivehimeVersion() {
        const url = `https://api.live.bilibili.com/xlive/app-blink/v1/liveVersionInfo/getHomePageLiveVersion?system_version=2`;

        try {
            const response = await makeApiRequest({
                method: 'GET',
                url: url
            });
            if (response.data && response.data.curr_version && response.data.build !== undefined) {
                 console.log("获取到最新直播姬版本信息:", {
                    version: response.data.curr_version,
                    build: response.data.build
                 });
                return {
                    version: response.data.curr_version,
                    build: response.data.build.toString() // 确保 build 是字符串类型
                };
            } else {
                throw new Error("API响应中缺少版本或构建号信息,或数据格式不正确。");
            }
        } catch (error) {
            displayResultMessage(`获取最新直播姬版本失败: ${error.message}`, 'error');
            console.error('获取最新直播姬版本失败:', error);
            throw error;
        }
    }

    // 2. 函数:获取房间信息 (RoomID, 当前分区等)
    async function fetchRoomInfo(forceRefresh = false) {
        if (currentRoomInfo && !forceRefresh) return currentRoomInfo.data;
        try {
            const response = await makeApiRequest({
                method: 'GET',
                url: 'https://api.live.bilibili.com/xlive/app-blink/v1/room/GetInfo?platform=pc'
            });
            currentRoomInfo = response;
            return response.data;
        } catch (error) {
            displayResultMessage(`获取房间信息失败: ${error.message}`, 'error');
            console.error('获取房间信息失败:', error);
            throw error;
        }
    }

    // 3. 函数:获取所有直播分区
    async function fetchAreaList() {
        if (availableAreas) return availableAreas.data;
        try {
            const response = await makeApiRequest({
                method: 'GET',
                url: 'https://api.live.bilibili.com/room/v1/Area/getList?show_pinyin=1'
            });
            availableAreas = response;
            return response.data;
        } catch (error) {
            displayResultMessage(`获取分区列表失败: ${error.message}`, 'error');
            console.error('获取分区列表失败:', error);
            throw error;
        }
    }

    // 4. 函数:执行开播请求 (已更新为带签名的版本,并动态获取 version/build)
    async function startLiveStream(roomId, areaV2) {
        const csrfToken = getCsrfToken();
        const dedeUserID = getDedeUserID(); // 获取用户UID
        if (!csrfToken || !dedeUserID) {
            displayResultMessage('错误:无法获取到 CSRF token 或用户UID。请确保您已登录(不可用)B站。', 'error');
            return;
        }

        // 固定的 AppKey 和 AppSecret (Salt)
        const APP_KEY = 'aae92bc66f3edfab';
        const APP_SECRET = 'af125a0d5279fd576c1b4418a3e8276d';

        // --- 获取最新版本和构建号 ---
        let latestVersionInfo;
        try {
            latestVersionInfo = await fetchLatestLivehimeVersion();
        } catch (error) {
            // fetchLatestLivehimeVersion 已经显示了错误信息,此处只需中断
            return;
        }

        const currentBuild = latestVersionInfo.build;
        const currentVersion = latestVersionInfo.version; // startLive API通常不直接使用 version 字符串,只用 build

        // 准备所有需要参与签名的参数
        const paramsToSign = new URLSearchParams();
        //paramsToSign.append('access_key', ''); // 根据算法,此项为空
        paramsToSign.append('appkey', APP_KEY);
        paramsToSign.append('area_v2', areaV2);
        paramsToSign.append('build', currentBuild);
        paramsToSign.append('version', currentVersion);
        paramsToSign.append('csrf', csrfToken);
        paramsToSign.append('csrf_token', csrfToken);
        paramsToSign.append('platform', 'pc_link');
        paramsToSign.append('room_id', roomId);
        paramsToSign.append('ts', Math.floor(Date.now() / 1000).toString());

        // 1. 对参数按 key 的字母顺序排序
        paramsToSign.sort();

        // 2. 拼接成字符串并附加 AppSecret
        const stringToSign = paramsToSign.toString() + APP_SECRET;

        // 3. 计算 MD5 哈希值,得到 sign
        const sign = md5(stringToSign);

        // 最终要发送的表单数据是已排序的参数 + sign
        const finalFormData = new URLSearchParams(paramsToSign); // 拷贝排序后的参数
        finalFormData.append('sign', sign);

        console.log('发送开播请求,数据:', Object.fromEntries(finalFormData));
        displayResultMessage('正在尝试开播,请稍候...', 'info', false); // 显示“正在尝试开播”信息,不自动关闭

        try {
            const data = await makeApiRequest({
                method: 'POST',
                url: 'https://api.live.bilibili.com/room/v1/Room/startLive',
                data: finalFormData.toString(),
            });
            await fetchRoomInfo(true); // 强制刷新房间信息

            if (data.data && data.data.rtmp) {
                const rtmpAddr = data.data.rtmp.addr;
                const rtmpCode = data.data.rtmp.code;
                const liveKey = data.data.live_key;
                const fullRtmpUrl = `${rtmpAddr}${rtmpCode}`;

                const messageHtml = `
                    <h4>开播成功!🎉</h4>
                    <div class="result-item">
                        <span>推流服务器 (Server):</span>
                        <input type="text" value="${rtmpAddr}" readonly id="rtmpAddrResult" />
                        <button class="copy-btn" data-clipboard-target="#rtmpAddrResult">复制</button>
                    </div>
                    <div class="result-item">
                        <span>串流密钥 (Stream Key):</span>
                        <input type="text" value="${rtmpCode}" readonly id="rtmpCodeResult" />
                        <button class="copy-btn" data-clipboard-target="#rtmpCodeResult">复制</button>
                    </div>
                     <div class="result-item">
                        <span>备用-直播密钥 (Live Key):</span>
                        <input type="text" value="${liveKey}" readonly id="liveKeyResult" />
                        <button class="copy-btn" data-clipboard-target="#liveKeyResult">复制</button>
                    </div>
                    <div class="result-item">
                        <span>完整推流地址 (Full RTMP):</span>
                        <input type="text" value="${fullRtmpUrl}" readonly id="fullRtmpResult" />
                        <button class="copy-btn" data-clipboard-target="#fullRtmpResult">复制</button>
                    </div>
                    <p style="font-size:0.9em; color: #555;">OBS等软件通常需要分别填写“服务器地址”和“串流密钥”。</p>
                `;
                displayResultMessage(messageHtml, 'success', false); // 成功信息不自动关闭
            } else {
                const errorDetail = `开播成功,但未找到完整的推流信息。API响应: <pre>${JSON.stringify(data, null, 2)}</pre>`;
                displayResultMessage(errorDetail, 'warning', true, 10000); // 警告可以自动关闭
            }
            hideAreaSelectionModal(); // 隐藏分区选择模态框
        } catch (error) {
            console.error('开播失败:', error);
            // 捕获身份验证错误码 60024
            if (error.message.includes('60024')) { // 人脸验证错误
                const faceAuthUrl = `https://www.bilibili.com/blackboard/live/face-auth-middle.html?source_event=400&mid=${dedeUserID}`;
                showAuthQRCodeModal(faceAuthUrl, roomId, areaV2); // 显示身份验证二维码模态框
                displayResultMessage('需要进行身份验证。请使用B站App扫描二维码完成验证。', 'warning', false); // 验证提示不自动关闭
            } else {
                displayResultMessage(`开播失败: ${error.message}`, 'error'); // 其他错误自动关闭
            }
        }
    }

    // 新增:执行关播请求的函数
    async function stopLiveStream() {
        const stopButton = document.getElementById('customStopLiveButton');
        if(stopButton) {
            stopButton.disabled = true;
            stopButton.textContent = '正在关播...';
        }

        const csrfToken = getCsrfToken();
        if (!csrfToken) {
            displayResultMessage('错误:无法获取到 CSRF token (bili_jct)。请确保您已登录(不可用)B站。', 'error');
            if(stopButton) {
                stopButton.disabled = false;
                stopButton.textContent = '一键关播';
            }
            return;
        }

        let roomIdToStop = null;
        try {
            const roomData = await fetchRoomInfo();
            roomIdToStop = roomData.room_id;
        } catch (e) {
            if(stopButton) {
                stopButton.disabled = false;
                stopButton.textContent = '一键关播';
            }
            return;
        }

        if (!roomIdToStop) {
            displayResultMessage('错误:无法获取房间ID以进行关播。', 'error');
            if(stopButton) {
                stopButton.disabled = false;
                stopButton.textContent = '一键关播';
            }
            return;
        }

        const platform = 'pc_link';
        const formData = new URLSearchParams();
        formData.append('room_id', roomIdToStop);
        formData.append('platform', platform);
        formData.append('csrf', csrfToken);
        formData.append('csrf_token', csrfToken);

        console.log('发送关播请求,数据:', Object.fromEntries(formData));
        displayResultMessage('正在尝试关播,请稍候...', 'info', false);

        try {
            const data = await makeApiRequest({
                method: 'POST',
                url: 'https://api.live.bilibili.com/room/v1/Room/stopLive',
                data: formData.toString(),
            });
            await fetchRoomInfo(true); // 强制刷新房间信息

            let message = `关播操作已发送。状态: ${data.data && data.data.status ? data.data.status : '未知'}`;
            if (data.code === 160000 || data.msg === "重复关播") { // 之前 makeApiRequest 已经处理了,但为了更清晰地展示,这里再次判断
                message = "当前直播间未在直播状态,或已成功关播。";
                displayResultMessage(message, 'info'); // 状态信息自动关闭
            } else if (data.code === 0) {
                message = `关播成功!当前状态: ${data.data && data.data.status ? data.data.status : 'PREPARING'}`;
                displayResultMessage(message, 'success'); // 成功信息自动关闭
            } else {
                 displayResultMessage(`关播响应异常: ${data.message || data.msg}`, 'warning');
            }
            console.log('关播API响应:', data);

        } catch (error) {
            displayResultMessage(`关播失败: ${error.message}`, 'error');
            console.error('关播失败:', error);
        } finally {
            if(stopButton) {
                stopButton.disabled = false;
                stopButton.textContent = '一键关播';
            }
        }
    }


    // 显示结果信息的函数
    function displayResultMessage(message, type = 'info', autoDismiss = true, duration = 5000) {
        let resultBox = document.getElementById('userscriptResultBox');
        if (!resultBox) {
            resultBox = document.createElement('div');
            resultBox.id = 'userscriptResultBox';
            document.body.appendChild(resultBox);

            const closeButton = document.createElement('button');
            closeButton.id = 'resultBoxCloseButton';
            closeButton.innerHTML = '×'; // HTML实体表示乘号 (X)
            closeButton.onclick = () => {
                resultBox.style.display = 'none';
                if (resultBoxTimeoutId) clearTimeout(resultBoxTimeoutId); // 如果手动关闭,清除定时器
            };
            resultBox.appendChild(closeButton);
        }

        let messageContent = resultBox.querySelector('.message-content');
        if (!messageContent) {
            messageContent = document.createElement('div');
            messageContent.className = 'message-content';
             if (resultBox.firstChild && resultBox.firstChild.id === 'resultBoxCloseButton' && resultBox.firstChild.nextSibling) {
                 resultBox.insertBefore(messageContent, resultBox.firstChild.nextSibling);
             } else if (resultBox.firstChild && resultBox.firstChild.id === 'resultBoxCloseButton') { // 如果只有关闭按钮
                 resultBox.appendChild(messageContent);
             }
             else { // 如果什么都没有
                 resultBox.appendChild(messageContent);
             }
        }

        messageContent.innerHTML = message;
        resultBox.className = ''; // 清除现有类
        resultBox.classList.add('userscript-result-box-base'); // 添加基础类
        resultBox.classList.add(`userscript-result-box-${type}`); // 添加类型特定类
        resultBox.style.display = 'block';

        messageContent.querySelectorAll('.copy-btn').forEach(button => {
            button.onclick = (e) => {
                const targetId = e.target.getAttribute('data-clipboard-target');
                const inputElement = document.querySelector(targetId);
                if (inputElement) {
                    inputElement.select();
                    inputElement.setSelectionRange(0, 99999);
                    try {
                        document.execCommand('copy');
                        e.target.textContent = '已复制!';
                        setTimeout(() => { e.target.textContent = '复制'; }, 1500);
                    } catch (err) {
                        console.error('复制失败:', err);
                        e.target.textContent = '复制失败';
                        setTimeout(() => { e.target.textContent = '复制'; }, 1500);
                    }
                    // 清除选择
                    if (window.getSelection) {
                        window.getSelection().removeAllRanges();
                    } else if (document.selection) {
                        document.selection.empty();
                    }
                }
            };
        });

        if (resultBoxTimeoutId) { // 清除任何现有定时器
            clearTimeout(resultBoxTimeoutId);
            resultBoxTimeoutId = null;
        }

        if (autoDismiss) {
            resultBoxTimeoutId = setTimeout(() => {
                if (resultBox) resultBox.style.display = 'none';
            }, duration);
        }
    }


    // 新增:生成并显示身份验证二维码的模态框
    function showAuthQRCodeModal(authUrl, roomId, areaV2) {
        let authModal = document.getElementById('authQRCodeModal');
        if (!authModal) {
            // 创建身份验证模态框的HTML结构
            const modalHTML = `
                <div id="authQRCodeModalOverlay"></div>
                <div id="authQRCodeModal">
                    <h2>身份验证</h2>
                    <p>请使用B站App扫描下方二维码进行身份验证。</p>
                    <div id="qrCodeContainer">
                        <img id="faceAuthQRCode" src="" alt="身份验证二维码">
                    </div>
                    <p class="small-text">(若二维码无法显示,请尝试复制链接在浏览器中打开:<a href="#" id="authUrlLink" target="_blank" rel="noopener noreferrer">点击此处</a>)</p>
                    <div id="authModalButtons">
                        <button id="authRetryBtn">我已验证,重新开播</button>
                        <button id="authCancelBtn">取消</button>
                    </div>
                    <p class="small-text caution-text">验证成功后,请点击“我已验证,重新开播”按钮。</p>
                </div>
            `;
            document.body.insertAdjacentHTML('beforeend', modalHTML);

            authModal = document.getElementById('authQRCodeModal');
            document.getElementById('authQRCodeModalOverlay').addEventListener('click', hideAuthQRCodeModal);
        }

        // 使用外部服务生成二维码图片URL
        const qrCodeImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(authUrl)}`;
        document.getElementById('faceAuthQRCode').src = qrCodeImageUrl;
        document.getElementById('authUrlLink').href = authUrl;

        // 设置“我已验证,重新开播”按钮的点击事件
        document.getElementById('authRetryBtn').onclick = async () => {
            hideAuthQRCodeModal();
            displayResultMessage('身份验证成功,正在重新尝试开播...', 'info', false);
            // 重新尝试开播,传递 roomId 和 areaV2
            await startLiveStream(roomId, areaV2);
        };
        // 设置“取消”按钮的点击事件
        document.getElementById('authCancelBtn').onclick = () => {
             hideAuthQRCodeModal();
             displayResultMessage('已取消开播操作,身份验证未完成。', 'info');
        };


        authModal.style.display = 'block';
        document.getElementById('authQRCodeModalOverlay').style.display = 'block';
    }

    // 新增:隐藏身份验证二维码模态框
    function hideAuthQRCodeModal() {
        const authModal = document.getElementById('authQRCodeModal');
        const authOverlay = document.getElementById('authQRCodeModalOverlay');
        if (authModal) authModal.style.display = 'none';
        if (authOverlay) authOverlay.style.display = 'none';
    }


    // 5. 创建和管理分区选择模态框
    function createAreaSelectionModal() {
        if (document.getElementById('areaSelectionModal')) return;

        const modalHTML = `
            <div id="areaSelectionModalOverlay"></div>
            <div id="areaSelectionModal">
                <h2>选择直播分区</h2>
                <div>
                    <label for="parentAreaSelect">父分区:</label>
                    <select id="parentAreaSelect"></select>
                </div>
                <div>
                    <label for="subAreaSelect">子分区:</label>
                    <select id="subAreaSelect"></select>
                </div>
                <div id="modalButtons">
                    <button id="confirmStartLiveBtn">确认开播</button>
                    <button id="cancelStartLiveBtn">取消</button>
                </div>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', modalHTML);

        const parentSelect = document.getElementById('parentAreaSelect');
        parentSelect.addEventListener('change', () => {
            const selectedParentId = parentSelect.value;
            // 确保 availableAreas 已加载
            if (availableAreas && availableAreas.data) {
                populateSubAreas(selectedParentId, availableAreas.data);
            } else {
                console.warn("分区列表数据未加载,无法填充子分区。");
            }
        });

        document.getElementById('confirmStartLiveBtn').addEventListener('click', async () => {
            const selectedSubAreaId = document.getElementById('subAreaSelect').value;
            if (!selectedSubAreaId) {
                displayResultMessage('请选择一个子分区!', 'warning');
                return;
            }
            let roomData;
            try {
                roomData = await fetchRoomInfo();
                if (!roomData || !roomData.room_id) {
                    displayResultMessage('无法获取房间ID,请重试。', 'error');
                    return;
                }
            } catch (e) {
                // fetchRoomInfo 已经显示错误信息,这里不用重复显示
                return;
            }

            document.getElementById('confirmStartLiveBtn').disabled = true;
            document.getElementById('confirmStartLiveBtn').textContent = '处理中...';
            // 调用开播函数,这里不需要 isRetry 参数,因为它只会在初次尝试或用户明确点击“重新开播”时被调用。
            await startLiveStream(roomData.room_id, selectedSubAreaId);
            document.getElementById('confirmStartLiveBtn').disabled = false;
            document.getElementById('confirmStartLiveBtn').textContent = '确认开播';
        });

        document.getElementById('cancelStartLiveBtn').addEventListener('click', hideAreaSelectionModal);
        document.getElementById('areaSelectionModalOverlay').addEventListener('click', hideAreaSelectionModal);
    }

    function populateParentAreas(areas, defaultParentId) {
        const parentSelect = document.getElementById('parentAreaSelect');
        parentSelect.innerHTML = '<option value="">--请选择父分区--</option>';
        if (!areas) {
            console.error("无法填充父分区:分区数据为空。");
            return;
        }
        areas.forEach(parentArea => {
            const option = document.createElement('option');
            option.value = parentArea.id;
            option.textContent = parentArea.name;
            parentSelect.appendChild(option);
        });
        if (defaultParentId) {
            parentSelect.value = defaultParentId;
        }
    }

    function populateSubAreas(parentId, allAreas, defaultSubId) {
        const subSelect = document.getElementById('subAreaSelect');
        subSelect.innerHTML = '<option value="">--请选择子分区--</option>';
        if (!parentId || !allAreas) return;

        const parent = allAreas.find(p => p.id.toString() === parentId.toString());
        if (parent && parent.list) {
            parent.list.forEach(subArea => {
                const option = document.createElement('option');
                option.value = subArea.id;
                option.textContent = subArea.name;
                subSelect.appendChild(option);
            });
        }
        if (defaultSubId) {
            subSelect.value = defaultSubId;
        }
    }

    async function showAreaSelectionModal() {
        if (!document.getElementById('areaSelectionModal')) {
            createAreaSelectionModal();
        }
        document.getElementById('areaSelectionModalOverlay').style.display = 'block';
        document.getElementById('areaSelectionModal').style.display = 'block';

        try {
            // 确保在填充前获取数据。
            // 这些调用会优先使用缓存数据,如果没有则进行请求。
            const roomData = await fetchRoomInfo();
            const areasData = await fetchAreaList();

            if (roomData && areasData) {
                populateParentAreas(areasData, roomData.parent_id);
                // 在父分区填充后,触发 change 事件
                document.getElementById('parentAreaSelect').dispatchEvent(new Event('change'));
                // 然后填充子分区,并可能设置默认值
                populateSubAreas(roomData.parent_id, areasData, roomData.area_v2_id);
            } else {
                throw new Error("加载模态框所需数据失败。");
            }
        } catch (e) {
            console.error("显示分区选择模态框时出错:", e);
            // fetch 函数可能已经显示了错误信息
            hideAreaSelectionModal();
        }
    }

    function hideAreaSelectionModal() {
        if (document.getElementById('areaSelectionModal')) {
            document.getElementById('areaSelectionModalOverlay').style.display = 'none';
            document.getElementById('areaSelectionModal').style.display = 'none';
        }
    }


    // 6. 创建并添加主按钮到页面
    function addActionButtons() {
        const startButton = document.createElement('button');
        startButton.id = 'customStartLiveAdvancedButton';
        startButton.textContent = '一键开播';
        startButton.addEventListener('click', async () => {
            startButton.disabled = true;
            startButton.textContent = '加载数据...';
            try {
                await showAreaSelectionModal();
            } catch (error) {
                console.error("开播按钮点击处理失败:", error);
            }
            startButton.disabled = false;
            startButton.textContent = '一键开播';
        });
        document.body.appendChild(startButton);

        const stopButton = document.createElement('button');
        stopButton.id = 'customStopLiveButton';
        stopButton.textContent = '一键关播';
        stopButton.addEventListener('click', stopLiveStream);
        document.body.appendChild(stopButton);

        console.log('B站开播/关播按钮已添加。');
    }

    // 7. 添加CSS样式
    GM_addStyle(`
        #customStartLiveAdvancedButton, #customStopLiveButton {
            position: fixed;
            right: 20px;
            z-index: 9998;
            padding: 10px 15px;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            transition: background-color 0.2s ease, opacity 0.2s ease;
        }
        #customStartLiveAdvancedButton {
            bottom: 120px;
            background-color: #fb7299;
        }
        #customStartLiveAdvancedButton:hover {
            background-color: #f0628a;
        }
        #customStopLiveButton {
            bottom: 70px;
            background-color: #757575;
        }
        #customStopLiveButton:hover {
            background-color: #616161;
        }
        #customStartLiveAdvancedButton:disabled, #customStopLiveButton:disabled {
            background-color: #ccc;
            cursor: not-allowed;
            opacity: 0.7;
        }

        #areaSelectionModalOverlay {
            display: none;
            position: fixed;
            top: 0; left: 0;
            width: 100%; height: 100%;
            background-color: rgba(0,0,0,0.5);
            z-index: 10000;
        }
        #areaSelectionModal {
            display: none;
            position: fixed;
            top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            background-color: white;
            padding: 20px 30px;
            border-radius: 8px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.3);
            z-index: 10001;
            min-width: 300px;
        }
        #areaSelectionModal h2 {
            margin-top: 0;
            margin-bottom: 20px;
            text-align: center;
            color: #333;
        }
        #areaSelectionModal div:not(#modalButtons) {
            margin-bottom: 15px;
            display: flex;
            align-items: center;
        }
        #areaSelectionModal label {
            display: inline-block;
            width: 80px;
            margin-right: 10px;
            color: #555;
            text-align: right;
            flex-shrink: 0;
        }
        #areaSelectionModal select {
            flex-grow: 1;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        #modalButtons {
            text-align: right;
            margin-top: 20px;
        }
        #modalButtons button {
            padding: 8px 15px;
            margin-left: 10px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        #confirmStartLiveBtn {
            background-color: #00aeec;
            color: white;
        }
        #confirmStartLiveBtn:hover {
            background-color: #0095cc;
        }
        #confirmStartLiveBtn:disabled {
            background-color: #ccc;
            cursor: not-allowed;
        }
        #cancelStartLiveBtn {
            background-color: #e7e7e7;
            color: #333;
        }
        #cancelStartLiveBtn:hover {
            background-color: #d0d0d0;
        }

        .userscript-result-box-base {
            display: none;
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 380px;
            padding: 15px;
            padding-top: 40px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 10002;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            font-size: 14px;
            line-height: 1.5;
            color: #333;
            box-sizing: border-box;
        }
        #resultBoxCloseButton {
            position: absolute;
            top: 10px;
            right: 10px;
            background: transparent;
            border: none;
            font-size: 24px;
            line-height: 1;
            cursor: pointer;
            color: #aaa;
            padding: 0;
        }
        #resultBoxCloseButton:hover {
            color: #333;
        }
        .userscript-result-box-info {
            background-color: #e6f7ff;
            border: 1px solid #91d5ff;
            color: #0050b3;
        }
        .userscript-result-box-success {
            background-color: #f6ffed;
            border: 1px solid #b7eb8f;
            color: #389e0d;
        }
        .userscript-result-box-success h4 {
            color: #237804;
        }
        .userscript-result-box-warning {
            background-color: #fffbe6;
            border: 1px solid #ffe58f;
            color: #ad6800;
        }
        .userscript-result-box-error {
            background-color: #fff1f0;
            border: 1px solid #ffa39e;
            color: #cf1322;
        }
        .userscript-result-box-base .message-content h4 {
            margin-top: 0;
            margin-bottom: 12px;
            font-size: 17px;
            font-weight: 600;
        }
        .userscript-result-box-base .result-item {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }
        .userscript-result-box-base .result-item span {
            flex-basis: 150px;
            flex-shrink: 0;
            font-weight: 500;
            margin-right: 8px;
            font-size: 0.9em;
            color: #555;
        }
        .userscript-result-box-base .result-item input[type="text"] {
            flex-grow: 1;
            padding: 7px 9px;
            border: 1px solid #d9d9d9;
            border-radius: 4px;
            font-size: 0.9em;
            background-color: #fff;
            color: #333;
            box-sizing: border-box;
        }
        .userscript-result-box-base .copy-btn {
            margin-left: 10px;
            padding: 6px 12px;
            font-size: 0.85em;
            background-color: #1890ff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.2s ease;
        }
        .userscript-result-box-base .copy-btn:hover {
            background-color: #40a9ff;
        }
        .userscript-result-box-base pre {
            background-color: #f5f5f5;
            padding: 10px;
            border-radius: 4px;
            border: 1px solid #e8e8e8;
            white-space: pre-wrap;
            word-break: break-all;
            max-height: 100px;
            overflow-y: auto;
            font-size: 0.85em;
            color: #595959;
        }

        /* 身份验证二维码模态框样式 */
        #authQRCodeModalOverlay {
            display: none;
            position: fixed;
            top: 0; left: 0;
            width: 100%; height: 100%;
            background-color: rgba(0,0,0,0.6);
            z-index: 20000; /* 确保在其他模态框之上 */
        }
        #authQRCodeModal {
            display: none;
            position: fixed;
            top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            background-color: white;
            padding: 25px 35px;
            border-radius: 10px;
            box-shadow: 0 8px 25px rgba(0,0,0,0.4);
            z-index: 20001;
            min-width: 350px;
            text-align: center;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            color: #333;
        }
        #authQRCodeModal h2 {
            margin-top: 5px;
            margin-bottom: 20px;
            color: #fb7299; /* Bilibili 粉色 */
            font-size: 22px;
        }
        #qrCodeContainer {
            margin: 20px auto;
            border: 2px solid #eee;
            width: 204px; /* 二维码尺寸 + 边框 */
            height: 204px;
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #f7f7f7;
            border-radius: 5px;
        }
        #faceAuthQRCode {
            width: 200px;
            height: 200px;
            display: block;
        }
        #authQRCodeModal p {
            font-size: 15px;
            color: #555;
            margin-bottom: 15px;
        }
        #authQRCodeModal p.small-text {
            font-size: 12px;
            color: #777;
            margin-top: -10px;
            margin-bottom: 20px;
        }
        #authQRCodeModal p.small-text a {
            color: #1890ff;
            text-decoration: none;
        }
        #authQRCodeModal p.small-text a:hover {
            text-decoration: underline;
        }
        #authModalButtons button {
            padding: 10px 20px;
            margin: 0 10px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.2s ease;
        }
        #authRetryBtn {
            background-color: #fb7299; /* Bilibili 粉色 */
            color: white;
        }
        #authRetryBtn:hover {
            background-color: #e06a8e;
        }
        #authCancelBtn {
            background-color: #e7e7e7;
            color: #333;
        }
        #authCancelBtn:hover {
            background-color: #d0d0d0;
        }
        #authQRCodeModal p.caution-text {
            color: #cf1322; /* 错误红色 */
            font-weight: bold;
            margin-top: 20px;
        }
    `);

    // 在文档加载完成后执行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', addActionButtons);
    } else {
        addActionButtons();
    }
})();

QingJ © 2025

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