Greasy Fork

来自缓存

Greasy Fork is available in English.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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('[弹幕检测] 页面加载完成,检测功能已启用');
    });
})();