Greasy Fork

Greasy Fork is available in English.

检测B站直播弹幕拦截(AI修复版)

修复之前版本无法发送弹幕和@的问题;优化了弹窗

// ==UserScript==
// @name         检测B站直播弹幕拦截(AI修复版)
// @namespace    http://tampermonkey.net/
// @version      1.2.1
// @description  修复之前版本无法发送弹幕和@的问题;优化了弹窗
// @author       熊孩子
// @match        https://live.bilibili.com/*
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('[弹幕检测] 脚本已加载');

    // 拦截fetch请求
    const originFetch = unsafeWindow.fetch;
    unsafeWindow.fetch = async function(input, init) {
        // 备份原始请求
        const originalRequest = () => originFetch.call(this, input, init);

        try {
            // 检查是否为弹幕发送请求
            const isMessageSend = typeof input === 'string' && (
                input.includes('/msg/send') ||
                input.includes('/api/sendmsg')
            );

            if (!isMessageSend) {
                return originalRequest();
            }

            console.log('[弹幕检测] 捕获到发送请求:', input);

            // 提取消息内容
            let msgContent = '';
            if (init && init.body) {
                try {
                    // 尝试不同格式解析
                    if (typeof init.body === 'string') {
                        const bodyParams = new URLSearchParams(init.body);
                        msgContent = bodyParams.get('msg') || bodyParams.get('text') || '未能获取消息内容';
                    } else if (init.body instanceof FormData) {
                        // 使用迭代器同步解析FormData
                        const bodyParams = new URLSearchParams();
                        for (const [key, value] of init.body.entries()) {
                            bodyParams.append(key, value);
                        }
                        msgContent = bodyParams.get('msg') || bodyParams.get('text') || '未能获取消息内容';
                    }
                } catch (e) {
                    console.warn('[弹幕检测] 解析请求体失败:', e);
                }
            }

            // 发送原始请求并检查结果
            const response = await originalRequest();
            const clonedResponse = response.clone();

            // 异步处理响应
            clonedResponse.json().then(result => {
                console.log('[弹幕检测] 响应数据:', result);

                // 统一处理code字段(兼容字符串/数字类型)
                const code = typeof result.code === 'string' ? parseInt(result.code) : result.code;

                // 更全面的响应检查
                if (code !== 0) {
                    // 一般错误情况
                    showPrompt("发送失败", `错误码: ${code}, 消息: ${result.message || result.msg || '未知错误'}`, msgContent);
                } else if (result.msg === "f" || result.data?.msg_type === -1) {
                    showPrompt("全站屏蔽", result.message || result.msg || '弹幕被全站屏蔽', msgContent);
                } else if (result.msg === "k" || result.data?.msg_type === -2) {
                    showPrompt("主播屏蔽", result.message || result.msg || '弹幕被主播屏蔽', msgContent);
                }
            }).catch(error => {
                console.error('[弹幕检测] 解析响应失败:', error);
            });

            return response;
        } catch (error) {
            console.error('[弹幕检测] 请求处理异常:', error);
            return originalRequest();
        }
    };

    // 拦截XMLHttpRequest
    const originXHROpen = XMLHttpRequest.prototype.open;
    const originXHRSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function() {
        this._dmUrl = arguments[1];
        return originXHROpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function(body) {
        // 检查是否是弹幕发送请求
        const isMessageSend = typeof this._dmUrl === 'string' && (
            this._dmUrl.includes('/msg/send') ||
            this._dmUrl.includes('/api/sendmsg')
        );

        if (isMessageSend) {
            console.log('[弹幕检测] 捕获到XHR发送请求:', this._dmUrl);

            // 尝试提取消息内容
            let msgContent = '';
            if (body) {
                try {
                    if (typeof body === 'string') {
                        const bodyParams = new URLSearchParams(body);
                        msgContent = bodyParams.get('msg') || bodyParams.get('text') || '未能获取消息内容';
                    } else if (body instanceof FormData) {
                        const bodyParams = new URLSearchParams();
                        for (const [key, value] of body.entries()) {
                            bodyParams.append(key, value);
                        }
                        msgContent = bodyParams.get('msg') || bodyParams.get('text') || '未能获取消息内容';
                    }
                } catch (e) {
                    console.warn('[弹幕检测] 解析XHR请求体失败:', e);
                }
            }

            // 保存消息内容
            this._dmContent = msgContent;

            // 监听响应
            const originalOnload = this.onload;
            this.onload = function() {
                try {
                    // 尝试解析响应
                    const result = JSON.parse(this.responseText);
                    console.log('[弹幕检测] XHR响应数据:', result);

                    // 统一处理code字段(兼容字符串/数字类型)
                    const code = typeof result.code === 'string' ? parseInt(result.code) : result.code;

                    // 检查响应状态
                    if (code !== 0) {
                        showPrompt("发送失败", `错误码: ${code}, 消息: ${result.message || result.msg || '未知错误'}`, this._dmContent);
                    } else if (result.msg === "f" || result.data?.msg_type === -1) {
                        showPrompt("全站屏蔽", result.message || result.msg || '弹幕被全站屏蔽', this._dmContent);
                    } else if (result.msg === "k" || result.data?.msg_type === -2) {
                        showPrompt("主播屏蔽", result.message || result.msg || '弹幕被主播屏蔽', this._dmContent);
                    }
                } catch (e) {
                    console.error('[弹幕检测] 解析XHR响应失败:', e);
                }

                // 调用原始的onload处理程序
                if (originalOnload) {
                    originalOnload.call(this);
                }
            };
        }

        return originXHRSend.apply(this, arguments);
    };

    // 改进的提示框显示函数
    function showPrompt(type, reason, msg) {
        console.log(`[弹幕检测] ${type}: ${reason}, 内容: ${msg}`);

        const showModal = () => {
            const modal = document.createElement('div');
            modal.style = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            padding: 20px;
            background: white;
            border: 2px solid red;
            z-index: 999999;
            box-shadow: 0 0 10px rgba(0,0,0,0.5);
            font-family: Arial, sans-serif;
            border-radius: 8px;
            max-width: 90%;
            width: 400px;
        `;

            // 创建一个唯一ID,避免ID冲突
            const textareaId = 'prompt-textarea-' + Date.now();
            const notificationId = 'copy-notification-' + Date.now();

            modal.innerHTML = `
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
                <h3 style="margin:0;color:#f25d8e;">弹幕被拦截 (${type})</h3>
                <span style="cursor:pointer;font-size:20px;" id="close-btn">×</span>
            </div>
            <div style="margin-bottom:10px;font-size:14px;color:#666;">${reason}</div>
            <p style="margin:5px 0;font-weight:bold;">弹幕内容:</p>
            <textarea
                id="${textareaId}"
                style="width:100%;min-width:300px;height:80px;margin:5px 0;padding:8px;border:1px solid #ddd;border-radius:4px;"
                readonly
            >${msg}</textarea>
            <div style="display:flex;justify-content:space-between;margin-top:10px;">
                <button
                    id="copy-btn"
                    style="background:#4CAF50;color:white;border:none;padding:8px 16px;border-radius:4px;cursor:pointer;"
                >复制</button>
                <button
                    id="confirm-btn"
                    style="background:#f25d8e;color:white;border:none;padding:8px 16px;border-radius:4px;cursor:pointer;"
                >确认</button>
            </div>
            <div id="${notificationId}" style="display:none;margin-top:10px;padding:8px;background:#e8f5e9;color:#2e7d32;text-align:center;border-radius:4px;">
                ✅ 已复制到剪贴板
            </div>
        `;

            // 添加到页面
            document.body.appendChild(modal);

            // 获取DOM元素引用
            const closeBtn = modal.querySelector('#close-btn');
            const copyBtn = modal.querySelector('#copy-btn');
            const confirmBtn = modal.querySelector('#confirm-btn');
            const textarea = modal.querySelector(`#${textareaId}`);
            const notification = modal.querySelector(`#${notificationId}`);

            // 绑定关闭事件
            closeBtn.addEventListener('click', () => modal.remove());
            confirmBtn.addEventListener('click', () => modal.remove());

            // 设置自动关闭定时器 (3秒后自动关闭)
            const autoCloseTimer = setTimeout(() => {
                if (document.body.contains(modal)) {
                    modal.remove();
                }
            }, 3000);

            // 绑定复制事件 - 直接使用事件监听器而不是内联onclick
            copyBtn.addEventListener('click', async function() {
                // 保存原始的 readonly 状态
                const isReadOnly = textarea.readOnly;

                try {
                    // 移除 readonly 属性以确保某些浏览器可以正常复制
                    textarea.readOnly = false;

                    // 选中文本
                    textarea.select();
                    textarea.setSelectionRange(0, 99999); // 兼容移动端

                    // 尝试使用新API复制
                    if (navigator.clipboard && window.isSecureContext) {
                        await navigator.clipboard.writeText(textarea.value);
                        showCopyNotification(notification);
                    }
                    // 回退到execCommand
                    else {
                        const successful = document.execCommand('copy');
                        if (successful) {
                            showCopyNotification(notification);
                        } else {
                            showCopyNotification(notification, false);
                        }
                    }
                } catch (err) {
                    console.error('复制失败:', err);
                    showCopyNotification(notification, false);
                } finally {
                    // 恢复原始的 readonly 状态
                    textarea.readOnly = isReadOnly;
                    // 取消焦点,避免干扰直播
                    textarea.blur();
                }
            });

            // 显示复制结果的非阻塞通知
            function showCopyNotification(element, success = true) {
                // 清除之前的自动关闭定时器
                clearTimeout(autoCloseTimer);

                // 设置通知内容
                element.textContent = success ? '✅ 已复制到剪贴板' : '❌ 复制失败,请手动复制';
                element.style.background = success ? '#e8f5e9' : '#ffebee';
                element.style.color = success ? '#2e7d32' : '#c62828';
                element.style.display = 'block';

                // 2秒后自动隐藏通知
                setTimeout(() => {
                    element.style.display = 'none';
                }, 2000);

                // 设置弹窗自动关闭 (复制后2秒)
                setTimeout(() => {
                    if (document.body.contains(modal)) {
                        modal.remove();
                    }
                }, 2000);
            }
        };

        // 确保DOM已加载
        if (document.body) {
            showModal();
        } else {
            document.addEventListener('DOMContentLoaded', showModal);
        }
    }

    // 页面加载完成后的通知
    document.addEventListener('DOMContentLoaded', () => {
        console.log('[弹幕检测] 页面加载完成,检测功能已启用');
    });
})();