ssby叔叔不约瓶子说小助手

适用于叔叔不约聊天网站的小工具

// ==UserScript==
// @name         ssby叔叔不约瓶子说小助手
// @namespace    https://www.shushubuyue.net/
// @version      2.7
// @description  适用于叔叔不约聊天网站的小工具
// @author       风过江
// @match        https://www.shushubuyue.net/*
// @language     zh-CN 
// @grant        none
// @license      CC BY-NC-ND 4.0
// ==/UserScript==

(function() {
    'use strict';
    let isRunning = false;          // 脚本运行状态(通过按钮控制)
    const MIN_DELAY = 800;          // 最小操作延迟(毫秒)
    const MAX_DELAY = 925;          // 最大操作延迟(毫秒)
    let greetingText = '您好';       // 打招呼内容(可自定义)
    let onlyMatchGirls = true;      // 仅匹配女生开关状态(默认开启)
    let contactQQ = '';             // 存储用户输入的QQ号
    let isSendingContact = false;   // 防重复发送锁(新增)

    // ---------------------- UI 界面(完整实现) ----------------------
    function createUIPanel() {
        // 创建控制面板容器
        const uiPanel = document.createElement('div');
        uiPanel.id = 'ssby-control-panel';
        uiPanel.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: white;
            padding: 16px 20px;
            border-radius: 10px;
            box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
            z-index: 999999;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            font-size: 14px;
            min-width: 240px;
            pointer-events: auto;
        `;

        // 标题
        const title = document.createElement('div');
        title.textContent = 'ssby小助手';
        title.style.cssText = `
            font-size: 18px;
            font-weight: 600;
            color: #1a202c;
            margin-bottom: 14px;
        `;

        // 打招呼内容输入框
        const greetingGroup = document.createElement('div');
        greetingGroup.style.marginBottom = '12px';

        const greetingLabel = document.createElement('div');
        greetingLabel.textContent = '打招呼内容:';
        greetingLabel.style.cssText = `
            color: #4a5568;
            margin-bottom: 6px;
            font-weight: 500;
        `;

        const greetingInput = document.createElement('input');
        greetingInput.type = 'text';
        greetingInput.value = greetingText;
        greetingInput.style.cssText = `
            width: 100%;
            padding: 8px 12px;
            border: 2px solid #e2e8f0;
            border-radius: 6px;
            font-size: 14px;
            transition: border-color 0.2s;
        `;
        greetingInput.placeholder = '输入打招呼内容(默认“您好”)';
        greetingInput.addEventListener('input', (e) => {
            greetingText = e.target.value.trim() || '您好';  // 输入为空时使用默认值
        });
        greetingInput.addEventListener('focus', () => greetingInput.style.borderColor = '#2d3748');
        greetingInput.addEventListener('blur', () => greetingInput.style.borderColor = '#e2e8f0');
        greetingGroup.append(greetingLabel, greetingInput);

        // QQ号输入框
        const contactGroup = document.createElement('div');
        contactGroup.style.marginBottom = '12px';

        const contactLabel = document.createElement('div');
        contactLabel.textContent = '联系方式(QQ号):';
        contactLabel.style.cssText = `color: #4a5568; margin-bottom: 6px; font-weight: 500;`;


        const contactInput = document.createElement('input');
        contactInput.type = 'text';
        contactInput.placeholder = '输入12位以内QQ号(仅数字)';
        contactInput.style.cssText = `
            width: 100%;
            padding: 8px 12px;
            border: 2px solid #e2e8f0;
            border-radius: 6px;
            font-size: 14px;
            transition: border-color 0.2s;
        `;
        contactInput.addEventListener('input', (e) => {
            contactQQ = e.target.value.replace(/\D/g, '').substring(0, 12); // 过滤非数字并限制12位
            e.target.value = contactQQ; // 输入框同步显示清理后的值
        });

        contactGroup.append(contactLabel, contactInput);

        // 发送联系方式按钮(修正添加位置)
        const sendContactBtn = document.createElement('button');
        sendContactBtn.textContent = '发送联系方式';
        sendContactBtn.style.cssText = `
            width: 100%;
            padding: 8px 12px;
            border: none;
            border-radius: 6px;
            background: #48bb78;
            color: white;
            font-size: 14px;
            cursor: pointer;
            margin-top: 8px;
        `;
        sendContactBtn.addEventListener('click', () => {
            if (!isSendingContact) sendContact(); // 防重复点击
        });
        contactGroup.appendChild(sendContactBtn); // 将按钮添加到QQ输入框组内(确保显示)

        // 仅匹配女生复选框
        const filterGroup = document.createElement('div');
        filterGroup.style.margin = '14px 0';

        const filterLabel = document.createElement('label');
        filterLabel.style.cssText = `
            display: flex;
            align-items: center;
            color: #4a5568;
            cursor: pointer;
            user-select: none;
        `;

        const filterCheckbox = document.createElement('input');
        filterCheckbox.type = 'checkbox';
        filterCheckbox.checked = onlyMatchGirls;
        filterCheckbox.style.cssText = `width: 18px; height: 18px; margin-right: 10px; cursor: pointer;`;
        filterCheckbox.addEventListener('change', (e) => {
            onlyMatchGirls = e.target.checked;
            //console.log(`[设置] 仅匹配女生:${onlyMatchGirls? '开启' : '关闭'}`);
        });

        const filterText = document.createElement('span');
        filterText.textContent = '仅匹配女生';
        filterText.style.fontSize = '15px';
        filterLabel.append(filterCheckbox, filterText);
        filterGroup.append(filterLabel);

        // 状态提示
        const statusText = document.createElement('div');
        statusText.id = 'status-text';
        statusText.textContent = '状态:已停止';
        statusText.style.cssText = `color: #e53e3e; margin: 12px 0; font-size: 15px;`;

        // 状态提示
        const tipsText = document.createElement('div');
        tipsText.id = 'tips-text';
        tipsText.textContent = 'tips:双击esc可自动离开';
        tipsText.style.cssText = `color: #3e3efe; margin: 12px 0; font-size: 15px;`;

        // 开始/停止控制按钮
        const controlBtn = document.createElement('button');
        controlBtn.textContent = '开始';
        controlBtn.style.cssText = `
            width: 100%;
            padding: 10px 12px;
            border: none;
            border-radius: 6px;
            background: #48bb78;
            color: white;
            font-size: 15px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
        `;
        controlBtn.onclick = () => {
            isRunning = !isRunning;
            if (isRunning) {
                controlBtn.textContent = '停止';
                controlBtn.style.background = '#e53e3e';
                statusText.textContent = '状态:运行中';
                statusText.style.color = '#48bb78';
                mainLoop().catch(err => {
                    statusText.textContent = `错误:${err.message}`;
                    statusText.style.color = 'red';
                    //console.error('主循环错误:', err);
                });
            } else {
                controlBtn.textContent = '开始';
                controlBtn.style.background = '#48bb78';
                statusText.textContent = '状态:已停止';
                statusText.style.color = '#e53e3e';
            }
        };
        controlBtn.addEventListener('mouseenter', () => {
            controlBtn.style.opacity = '0.9';
            controlBtn.style.transform = 'scale(1.02)';
        });
        controlBtn.addEventListener('mouseleave', () => {
            controlBtn.style.opacity = '1';
            controlBtn.style.transform = 'scale(1)';
        });

        // 组装UI面板
        uiPanel.append(title, greetingGroup, contactGroup, filterGroup, statusText, tipsText, controlBtn);
        document.body.appendChild(uiPanel);
    }

    // ---------------------- 工具函数(完整实现) ----------------------
    function randomDelay() {
        return Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY;
    }

    async function typeLikeHuman(inputElement, text) {
        for (let i = 0; i < text.length; i++) {
            inputElement.value += text[i];
            inputElement.dispatchEvent(new Event('input', { bubbles: true }));
            await new Promise(resolve => setTimeout(resolve, Math.random() * 75 + 25));
        }
    }

    async function waitForElement(selector) {
        return new Promise(resolve => {
            const checkInterval = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(checkInterval);
                    resolve(element);
                }
            }, 150);
        });
    }

    async function waitForPopupDisappear(popupSelector, timeout = 3000) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            const checkInterval = setInterval(() => {
                const popup = document.querySelector(popupSelector);
                const isDisappeared =!popup || (popup.offsetParent === null);
                if (isDisappeared) {
                    clearInterval(checkInterval);
                    resolve();
                } else if (Date.now() - startTime > timeout) {
                    clearInterval(checkInterval);
                    reject(new Error(`弹窗在${timeout}ms内未消失`));
                }
            }, 100);
        });
    }

    function simulateRealClick(element) {
        if (!element) return;
        const rect = element.getBoundingClientRect();
        const clickEvent = new MouseEvent('click', {
            bubbles: true,
            cancelable: true,
            view: window,
            clientX: rect.left + rect.width / 2,
            clientY: rect.top + rect.height / 2,
            button: 0
        });
        element.dispatchEvent(clickEvent);
    }

    function simulateClickCenterBottom() {
        const centerX = window.innerWidth / 2;
        const bottomY = document.documentElement.clientHeight - 50;
        document.body.dispatchEvent(new MouseEvent('click', {
            bubbles: true,
            clientX: centerX,
            clientY: bottomY
        }));
        //console.log(`[ESC触发] 点击位置:横向中间(${Math.round(centerX)}px),纵向底部50px(${bottomY}px)`);
    }

    function initEscListener() {
        let lastEscTime = 0;
        document.addEventListener('keydown', (event) => {
            if (event.key!== 'Escape') return;
            const now = Date.now();
            if (now - lastEscTime < 500) return;

            const targetSelectors = [
                'span.actions-modal-button.actions-modal-button-bold.color-danger',
                '.button-link.chat-control',
                'span.chat-control'
            ];

            let clickedElement = null;
            for (const selector of targetSelectors) {
                const element = document.querySelector(selector);
                if (element) {
                    simulateRealClick(element);
                    clickedElement = selector;
                    break;
                }
            }

            if (clickedElement) {
                console.log(`[ESC触发] 已点击目标元素:${clickedElement}`);
            } else {
                simulateClickCenterBottom();
                console.log('[ESC触发] 未找到目标元素,已点击页面底部中间');
            }

            lastEscTime = now;
        });
        //console.log('[初始化] ESC键监听已启动(防抖500ms)');
    }

    // ---------------------- 核心匹配逻辑 ----------------------
    function validatePartnerInfo() {
        const partnerInfoElement = document.getElementById('partnerInfoText');
        if (!partnerInfoElement) {
            //console.log('[警告] 未找到对方信息元素(partnerInfoText)');
            return false;
        }

        const partnerInfoText = partnerInfoElement.textContent.trim();
        if (onlyMatchGirls) {
            const isGirl = partnerInfoText.includes('女生');
            //console.log(`[匹配检查] 对方信息:“${partnerInfoText}” → 女生匹配结果:${isGirl}`);
            return isGirl;
        } else {
            //console.log(`[匹配检查] 对方信息:“${partnerInfoText}” → 性别不限制,通过`);
            return true;
        }
    }

    // ---------------------- 核心流程 ----------------------
    async function mainLoop() {
        if (!isRunning) return;

        try {
            const msgInput = document.querySelector('textarea#msgInput.col-100[placeholder="输入信息······"]');

            if (!msgInput) {
                const chatControlSpan = document.querySelector('span.chat-control');
                if (chatControlSpan) {
                    simulateRealClick(chatControlSpan);
                    //console.log('[流程] 输入框不存在,点击聊天控制按钮');
                }
            } else {
                const isPartnerValid = validatePartnerInfo();

                if (!isPartnerValid) {
                    const specialButton = document.querySelector('.button-link.chat-control');
                    if (specialButton) {
                        simulateRealClick(specialButton);
                        //console.log('[流程] 点击聊天控制按钮(准备离开)');

                        const leaveSpan = await waitForElement('span.actions-modal-button.actions-modal-button-bold.color-danger');
                        if (leaveSpan) {
                            await new Promise(resolve => setTimeout(resolve, randomDelay()));
                            simulateRealClick(leaveSpan);
                            //console.log('[流程] 点击离开按钮');

                            try {
                                await waitForPopupDisappear('.actions-modal', 3000);
                                //console.log('[成功] 离开弹窗已消失');
                            } catch (error) {
                                //console.error('[警告] 离开弹窗未及时消失:', error.message);
                            }
                        }
                    }
                } else {
                    if (msgInput.value!== greetingText) {
                        await typeLikeHuman(msgInput, greetingText);
                        //console.log(`[流程] 输入打招呼内容:“${greetingText}”`);
                    }

                    const sendButton = document.querySelector('.button-link.msg-send');
                    if (sendButton) {
                        simulateRealClick(sendButton);
                        console.log('[流程] 点击发送按钮');
                        msgInput.focus();
                    }

                    // 等待聊天控制按钮恢复循环
                    //console.log('[等待] 检测聊天控制按钮以恢复循环...');
                    await waitForElement('span.chat-control');
                    //console.log('[继续] 检测到聊天控制按钮,恢复主循环');
                }
            }

            if (isRunning) {
                setTimeout(mainLoop, randomDelay());
            }
        } catch (error) {
            //console.error('[主循环错误]', error);
            statusText.textContent = `错误:${error.message}`;
            statusText.style.color = 'red';
        }
    }

    // ---------------------- 新增功能:发送联系方式 ----------------------
    async function sendContact() {
        if (isSendingContact) return; // 防重复发送
        isSendingContact = true;

        try {
            const msgInput = document.querySelector('textarea#msgInput.col-100[placeholder="输入信息······"]');
            if (!msgInput) throw new Error('未找到消息输入框');
            if (contactQQ.length === 0) throw new Error('请先输入QQ号');

            const batchSize = 3;
            for (let i = 0; i < contactQQ.length; i += batchSize) {
                const batch = contactQQ.substr(i, batchSize);
                msgInput.value = '';                  // 清空输入框
                await typeLikeHuman(msgInput, batch);  // 模拟人类输入
                await new Promise(resolve => setTimeout(resolve, randomDelay())); // 随机延迟
                const sendButton = document.querySelector('.button-link.msg-send');
                sendButton && simulateRealClick(sendButton); // 点击发送按钮
                //console.log(`[联系方式] 发送批次:${batch}`);
            }
        } catch (error) {
            //console.error('[发送错误]', error.message);
        } finally {
            isSendingContact = false; // 解锁
        }
    }

    // ---------------------- 新增功能:“·”键监听 ----------------------
    function initPeriodKeyListener() {
        document.addEventListener('keydown', (e) => {
            // 英文状态下的“·”键(非中文句号,且未在发送中)
            if (e.key === '`' && !e.shiftKey && !isSendingContact) {
                e.preventDefault(); // 防止浏览器默认行为(如聚焦地址栏)
                sendContact(); // 触发发送
            }
        });
        //console.log('[初始化] “·”键监听已启动(英文状态有效)');
    }

    // ---------------------- 初始化 ----------------------
    createUIPanel();
    initEscListener();
    initPeriodKeyListener(); // 启动“·”键监听
})();