BOSS海投助手

🚀 求职者自己的神器!🧑‍💻Yangshengzhou,提升BOSS直聘的简历投递效率,自动批量发送简历,高效求职 💼

// ==UserScript==
// @name         BOSS海投助手
// @namespace    https://github.com/yangshengzhou03
// @version      1.2.2
// @description  🚀 求职者自己的神器!🧑‍💻Yangshengzhou,提升BOSS直聘的简历投递效率,自动批量发送简历,高效求职 💼
// @author       Yangshengzhou
// @match        https://www.zhipin.com/web/*
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// @supportURL   https://github.com/yangshengzhou03
// @homepageURL  https://gitee.com/yangshengzhou
// @license      AGPL-3.0-or-later
//               更多详情请参见: https://www.gnu.org/licenses/agpl-3.0.html
// @icon         https://static.zhipin.com/favicon.ico
// @connect      zhipin.com
// @connect      spark-api-open.xf-yun.com
// @noframes
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// ==/UserScript==

(function () {
    'use strict';

    // 配置常量
    const CONFIG = {
        INTERVAL: 2500,
        CARD_STYLE: {
            BACKGROUND: '#ffffff',
            SHADOW: '0 6px 18px rgba(0,0,0,0.12)',
            BORDER: '1px solid #e4e7ed'
        },
        COLORS: {
            PRIMARY: '#2196f3',
            SECONDARY: '#ff5722',
            NEUTRAL: '#95a5a6'
        },
        MINI_ICON_SIZE: 40
    };

    // 状态管理
    const state = {
        isRunning: false,
        currentIndex: 0,
        filterKeyword: '',
        locationKeyword: '',
        jobList: [],
        isMinimized: false,
        processedHRs: new Set(JSON.parse(localStorage.getItem('processedHRs') || '[]')),
        currentTopHRKey: null,
        aiReplyCount: JSON.parse(localStorage.getItem('aiReplyCount') || '0'),
        lastAiDate: localStorage.getItem('lastAiDate') || ''
    };

    // DOM 元素引用
    const elements = {
        panel: null,
        controlBtn: null,
        log: null,
        filterInput: null,
        miniIcon: null
    };

    // UI 模块:负责创建和操作界面组件
    const UI = {

        // 创建主控制面板
        createControlPanel() {
            if (document.getElementById('boss-pro-panel')) return;

            elements.panel = this._createPanel();
            const header = this._createHeader();
            const controls = this._createControls();
            elements.log = this._createLogger();
            const footer = this._createFooter();

            elements.panel.append(header, controls, elements.log, footer);
            document.body.appendChild(elements.panel);

            this._makeDraggable(elements.panel);

            if (!document.getElementById('boss-settings-dialog')) {
                const settingsDialog = this._createSettingsDialog();
                document.body.appendChild(settingsDialog);
            }
        },

        // 创建面板容器
        _createPanel() {
            const panel = document.createElement('div');
            panel.id = 'boss-pro-panel';
            panel.className = 'boss-pro-panel';
            panel.style.cssText = `
                position: fixed;
                top: 36px;
                right: 24px;
                width: 380px;
                background: linear-gradient(145deg, #ffffff, #f9f9fc);
                border-radius: 16px;
                box-shadow: 0 10px 25px rgba(0,0,0,0.1);
                padding: 18px;
                font-family: 'Segoe UI', system-ui, sans-serif;
                z-index: 2147483647;
                cursor: move;
                display: flex;
                flex-direction: column;
                transition: all 0.3s ease;
            `;
            return panel;
        },

        // 创建面板头部
        _createHeader() {
            const header = document.createElement('div');
            header.className = 'boss-header';
            header.style.cssText = `
        display: flex;
        justify-content: space-between;
        margin-bottom: 1.5rem;
    `;

            const title = document.createElement('div');
            title.style.display = 'flex';
            title.style.flexDirection = 'column';
            title.style.gap = '0.3em';

            title.innerHTML = `
        <h3 style="
            margin: 0;
            color: #2c3e50;
            font-weight: 660;
            font-size: 1.2rem;
        ">
            <span style="color:${CONFIG.COLORS.PRIMARY};">BOSS</span>海投助手
        </h3>
        <span style="
            font-size:0.8em;
            color:${CONFIG.COLORS.NEUTRAL};
        ">
            v1.2-Beta (Professional)
        </span>
    `;

            const closeBtn = this._createIconButton('✕', () => {
                state.isMinimized = true;
                elements.panel.style.transform = 'translateY(160%)';
                elements.miniIcon.style.display = 'flex';
            });
            closeBtn.style.color = CONFIG.COLORS.NEUTRAL;
            closeBtn.style.marginTop = '0.2rem';
            closeBtn.title = '最小化海投面板';

            header.append(title, closeBtn);
            return header;
        },

        // 创建控制区域(按钮 + 输入框)
        _createControls() {
            const container = document.createElement('div');
            container.className = 'boss-controls';
            container.style.marginBottom = '0.8rem';

            const utilGroup = document.createElement('div');
            utilGroup.className = 'boss-util-group';
            utilGroup.style.cssText = `
        display: flex;
        gap: 10px;
        justify-content: flex-end;
        margin-top: 1rem;
    `;

            const clearLogBtn = this._createIconButton('🗑', () => {
                elements.log.innerHTML = `<div style="color:${CONFIG.COLORS.NEUTRAL}; margin-bottom:8px;">请在BOSS中展示岗位列表,我将自动与这些岗位沟通。</div>`;
            });
            clearLogBtn.title = '清空日志';

            const settingsBtn = this._createIconButton('⚙', () => {
                const dialog = document.getElementById('boss-settings-dialog');
                if (dialog) {
                    dialog.style.display = 'flex';
                } else {
                    const newDialog = this._createSettingsDialog();
                    document.body.appendChild(newDialog);
                    newDialog.style.display = 'flex';
                }
            });
            settingsBtn.title = '插件设置';

            const joinGroupBtn = this._createIconButton('✉', () => {
                window.open('https://qun.qq.com/universal-share/share?ac=1&authKey=twqkV%2Fu9zC7%2FrSMJOOxD%2FbM4rXDqkE2zWg%2FmR6DdVI9BJImtoBFqvNnE9F3KyW3P&busi_data=eyJncm91cENvZGUiOiIxMDQ3ODQyNDczIiwidG9rZW4iOiJBTGF3eEVwLzYvRCtHRkR2WnVDYUVFTDdJQWY0cGVDODl4amlkTkVaL2R4bTU1WE9CSTUzSzRYTkhvdW9DTGVKIiwidWluIjoiMzU1NTg0NDY3OSJ9&data=WOu9pw6Y-e2H4ebyGMMgxV75yzExdNk2LrecdfSDL1RVZ9TbUOvXDVzo3qIShDAzhK5PjDYx5o-uXH-GdA0FfQ&svctype=4&tempid=h5_group_info', '_blank');
            });
            joinGroupBtn.title = '加入粉丝交流群';

            utilGroup.append(clearLogBtn, settingsBtn, joinGroupBtn);

            const jobLabel = document.createElement('label');
            jobLabel.textContent = '岗位筛选:';
            jobLabel.style.cssText = 'display:block; margin-bottom:0.5rem;';

            elements.filterInput = document.createElement('input');
            elements.filterInput.id = 'job-filter';
            elements.filterInput.placeholder = '岗位包含词(英文逗号分隔,为空则不限)';
            elements.filterInput.className = 'boss-filter-input';
            elements.filterInput.style.cssText = `
        width: calc(100%);
        padding: 12px 16px;
        border-radius: 10px;
        border: 2px solid #ddd;
        margin-bottom: 1rem;
        font-size: 14px;
        transition: border-color 0.3s ease;
        outline: none;
    `;

            elements.filterInput.addEventListener('focus', () => {
                elements.filterInput.style.borderColor = CONFIG.COLORS.PRIMARY;
            });

            elements.filterInput.addEventListener('blur', () => {
                elements.filterInput.style.borderColor = '#ddd';
            });

            const locationLabel = document.createElement('label');
            locationLabel.textContent = '岗位地点:';
            locationLabel.style.cssText = 'display:block; margin-bottom:0.5rem;';

            elements.locationInput = document.createElement('input');
            elements.locationInput.id = 'location-filter';
            elements.locationInput.placeholder = '岗位所在地(英文逗号分隔,为空则不限)';
            elements.locationInput.className = 'boss-filter-input';
            elements.locationInput.style.cssText = `
        width: calc(100%);
        padding: 12px 16px;
        border-radius: 10px;
        border: 2px solid #ddd;
        margin-bottom: 1rem;
        font-size: 14px;
        transition: border-color 0.3s ease;
        outline: none;
    `;

            elements.locationInput.addEventListener('focus', () => {
                elements.locationInput.style.borderColor = CONFIG.COLORS.PRIMARY;
            });

            elements.locationInput.addEventListener('blur', () => {
                elements.locationInput.style.borderColor = '#ddd';
            });

            elements.controlBtn = this._createTextButton(
                '启动海投',
                `linear-gradient(45deg, ${CONFIG.COLORS.PRIMARY}, #4db6ac)`,
                () => {
                    toggleProcess();
                }
            );

            container.append(
                jobLabel,
                elements.filterInput,
                locationLabel,
                elements.locationInput,
                elements.controlBtn,
                utilGroup
            );

            return container;
        },



        // 创建日志区域
        _createLogger() {
            const log = document.createElement('div');
            log.id = 'pro-log';
            log.className = 'boss-log';
            log.style.cssText = `
                height: 200px;
                overflow-y: auto;
                background: #f8f9fa;
                border-radius: 10px;
                padding: 12px;
                border: 1px solid #eceff1;
                font-size: 13px;
                line-height: 1.5;
                margin-bottom: 1rem;
                transition: all 0.3s ease;
                user-select: text;
            `;

            log.innerHTML = `
                <div style="color:${CONFIG.COLORS.NEUTRAL}; margin-bottom:8px;">
                    请在BOSS中展示岗位列表,我将自动与这些岗位沟通。
                </div>
            `;

            return log;
        },

        // 创建底部版权信息
        _createFooter() {
            const footer = document.createElement('div');
            footer.textContent = '© 2025 Yangshengzhou · All Rights Reserved';
            footer.style.cssText = `
                text-align: center;
                font-size: 0.8em;
                color: ${CONFIG.COLORS.NEUTRAL};
                padding-top: 10px;
                border-top: 1px solid #eee;
                margin-top: auto;
                transition: color 0.3s ease;
            `;
            return footer;
        },

        // 创建文本按钮
        _createTextButton(text, bgColor, onClick) {
            const btn = document.createElement('button');
            btn.className = 'boss-btn';
            btn.textContent = text;
            btn.style.cssText = `
                width: 100%;
                padding: 12px 16px;
                background: ${bgColor};
                color: #fff;
                border: none;
                border-radius: 10px;
                cursor: pointer;
                font-size: 15px;
                font-weight: 500;
                transition: all 0.3s ease;
                display: flex;
                justify-content: center;
                align-items: center;
                box-shadow: 0 4px 10px rgba(0,0,0,0.1);
            `;

            btn.addEventListener('click', onClick);
            btn.addEventListener('mouseenter', () => {
                btn.style.transform = 'scale(1.03)';
                btn.style.boxShadow = '0 6px 15px rgba(0,0,0,0.15)';
            });
            btn.addEventListener('mouseleave', () => {
                btn.style.transform = 'scale(1)';
                btn.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)';
            });

            return btn;
        },

        // 创建图标按钮
        _createIconButton(icon, onClick) {
            const btn = document.createElement('button');
            btn.className = 'boss-icon-btn';
            btn.innerHTML = icon;
            btn.style.cssText = `
                width: 36px;
                height: 36px;
                border-radius: 50%;
                border: none;
                background: #f0f0f0;
                cursor: pointer;
                font-size: 18px;
                transition: all 0.2s ease;
                display: flex;
                justify-content: center;
                align-items: center;
                color: ${CONFIG.COLORS.PRIMARY};
                transform: translateY(8px);
            `;

            btn.addEventListener('click', onClick);
            btn.addEventListener('mouseenter', () => {
                btn.style.backgroundColor = CONFIG.COLORS.PRIMARY;
                btn.style.color = '#fff';
                btn.style.transform = 'translateY(8px) scale(1.1)';
            });
            btn.addEventListener('mouseleave', () => {
                btn.style.backgroundColor = '#f0f0f0';
                btn.style.color = CONFIG.COLORS.PRIMARY;
                btn.style.transform = 'translateY(8px)';
            });

            return btn;
        },

        // 设置面板可拖动功能
        _makeDraggable(panel) {
            const draggableAreaHeightRatio = 0.3;

            panel.addEventListener('mousemove', (e) => {
                const rect = panel.getBoundingClientRect();
                const relativeY = e.clientY - rect.top;
                const draggableHeight = rect.height * draggableAreaHeightRatio;

                if (relativeY <= draggableHeight) {
                    panel.style.cursor = 'move';
                } else {
                    panel.style.cursor = 'default';
                }
            });

            let isDragging = false;
            let startX = 0, startY = 0;
            let initialX = panel.offsetLeft, initialY = panel.offsetTop;

            panel.addEventListener('mousedown', (e) => {
                const rect = panel.getBoundingClientRect();
                const relativeY = e.clientY - rect.top;
                const draggableHeight = rect.height * draggableAreaHeightRatio;

                if (relativeY <= draggableHeight) {
                    isDragging = true;
                    startX = e.clientX;
                    startY = e.clientY;
                    initialX = panel.offsetLeft;
                    initialY = panel.offsetTop;
                    panel.style.transition = 'none';
                }
            });

            document.addEventListener('mousemove', (e) => {
                if (!isDragging) return;

                const dx = e.clientX - startX;
                const dy = e.clientY - startY;

                panel.style.left = `${initialX + dx}px`;
                panel.style.top = `${initialY + dy}px`;
                panel.style.right = 'auto';
            });

            document.addEventListener('mouseup', () => {
                if (isDragging) {
                    isDragging = false;
                    panel.style.transition = 'all 0.3s ease';
                }
            });
        },

        // 创建最小化图标
        createMiniIcon() {
            elements.miniIcon = document.createElement('div');
            elements.miniIcon.style.cssText = `
                width: ${CONFIG.MINI_ICON_SIZE}px;
                height: ${CONFIG.MINI_ICON_SIZE}px;
                position: fixed;
                bottom: 40px;
                left: 40px;
                background: linear-gradient(135deg, ${CONFIG.COLORS.PRIMARY}, #4db6ac);
                border-radius: 50%;
                box-shadow: 0 6px 16px rgba(33, 150, 243, 0.4);
                cursor: pointer;
                display: none;
                justify-content: center;
                align-items: center;
                color: #fff;
                font-size: 18px;
                z-index: 2147483647;
                transition: all 0.3s ease;
                overflow: hidden;
                text-align: center;
                line-height: 1;
            `;

            elements.miniIcon.innerHTML = '↗';

            elements.miniIcon.addEventListener('mouseenter', () => {
                elements.miniIcon.style.transform = 'scale(1.1)';
                elements.miniIcon.style.boxShadow = '0 8px 20px rgba(33, 150, 243, 0.5)';
            });

            elements.miniIcon.addEventListener('mouseleave', () => {
                elements.miniIcon.style.transform = 'scale(1)';
                elements.miniIcon.style.boxShadow = '0 6px 16px rgba(33, 150, 243, 0.4)';
            });

            elements.miniIcon.addEventListener('click', () => {
                state.isMinimized = false;
                elements.panel.style.transform = 'translateY(0)';
                elements.miniIcon.style.display = 'none';
            });

            document.body.appendChild(elements.miniIcon);
        },

        _createSettingsDialog() {
            const dialog = document.createElement('div');
            dialog.id = 'boss-settings-dialog';
            dialog.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 420px;
        background: #fff;
        border-radius: 12px;
        box-shadow: 0 10px 30px rgba(0,0,0,0.15);
        padding: 20px;
        z-index: 999999;
        display: none;
        flex-direction: column;
        gap: 12px;
        font-family: 'Segoe UI', sans-serif;
    `;

            // 标题
            const title = document.createElement('h4');
            title.textContent = '海投设置';
            title.style.marginTop = 0;
            title.style.color = CONFIG.COLORS.PRIMARY;

            // AI 人设输入框
            const roleLabel = document.createElement('label');
            roleLabel.textContent = 'AI 人设(如:“你是一位正在求职的资深全栈工程师”):';

            const roleInput = document.createElement('textarea');
            roleInput.id = 'ai-role-input';
            roleInput.rows = 4;
            roleInput.value = localStorage.getItem('aiRole') || '你是有工作经验的求职者,擅长交流。你会用一些口语化的表达(如“嗯”、“呃”)和语气词(如“啊”、“呢”),使对话听起来自然。遇到不清楚的问题,你会请求对方解释,确保准确理解。回复简洁明了,避免冗长复杂的句子结构。';
            roleInput.style.cssText = `
        width: 100%;
        padding: 10px;
        border-radius: 8px;
        border: 1px solid #ccc;
        resize: vertical;
        font-size: 13px;
        margin-bottom: 10px;
    `;

            // 保存按钮
            const saveBtn = this._createTextButton('保存设置', `linear-gradient(45deg, ${CONFIG.COLORS.PRIMARY}, #4db6ac)`, () => {
                const aiRole = roleInput.value.trim();
                if (aiRole) {
                    localStorage.setItem('aiRole', aiRole);
                    alert('设置保存成功!');
                    dialog.style.display = 'none';
                }
            });

            // 取消按钮
            const cancelBtn = this._createTextButton('取消', '#ccc', () => {
                dialog.style.display = 'none';
            });
            cancelBtn.style.marginLeft = '10px';

            const btnGroup = document.createElement('div');
            btnGroup.style.cssText = `
        display: flex;
        justify-content: flex-end;
        gap: 10px;
        margin-top: 10px;
    `;
            btnGroup.append(saveBtn, cancelBtn);

            // 前往 Gitee 按钮
            const giteeBtn = this._createTextButton('前往 Gitee 看源码', '#68130d', () => {
                window.open('https://gitee.com/Yangshengzhou', '_blank');
            });
            giteeBtn.style.width = '100%';
            giteeBtn.style.marginTop = '15px';

            // 插入元素
            dialog.append(title, roleLabel, roleInput, btnGroup, giteeBtn);

            return dialog;
        }
    };

    const Core = {
        async startProcessing() {
            if (location.pathname.includes('/jobs'))
                await this.autoScrollJobList();
            while (state.isRunning) {
                if (location.pathname.includes('/jobs'))
                    await this.processJobList();
                else if (location.pathname.includes('/chat'))
                    await this.handleChatPage();
                await this.delay(CONFIG.INTERVAL);
            }
        },

        async autoScrollJobList() {
            return new Promise((resolve) => {
                const cardSelector = 'li.job-card-box'; // 岗位卡片的选择器
                const maxHistory = 3; // 最大历史记录数,用于判断是否加载完成
                const waitTime = 550; // 等待时间(ms),每次滚动后等待的时间以便新内容加载
                let cardCountHistory = []; // 存储过去几次的岗位卡片数量
                let isStopped = false; // 标识是否应该停止滚动

                const scrollStep = async () => {
                    if (isStopped) return; // 如果标记为停止,则退出递归

                    window.scrollTo({
                        top: document.documentElement.scrollHeight,
                        behavior: 'smooth'
                    }); // 平滑滚动到底部

                    await this.delay(waitTime); // 等待指定时间让新内容加载

                    const cards = document.querySelectorAll(cardSelector); // 获取当前页面上所有岗位卡片
                    const currentCount = cards.length; // 当前岗位卡片的数量
                    cardCountHistory.push(currentCount); // 记录当前岗位卡片数量

                    if (cardCountHistory.length > maxHistory) cardCountHistory.shift(); // 维护历史记录长度

                    // 判断如果最近maxHistory次记录中的值都相同(即没有新岗位卡片加载进来)
                    if (cardCountHistory.length === maxHistory && new Set(cardCountHistory).size === 1) {
                        this.log("当前页面岗位加载完成,开始沟通");
                        resolve(cards); // 完成Promise,返回所有岗位卡片
                        return;
                    }

                    scrollStep(); // 递归调用自身,继续滚动
                };

                scrollStep(); // 开始首次滚动

                // 提供一个方法来从外部停止滚动
                this.stopAutoScroll = () => {
                    isStopped = true;
                    resolve(null);
                };
            });
        },

        // 点击Job页面的立即沟通按钮
        async processJobList() {
            state.jobList = Array.from(document.querySelectorAll('li.job-card-box'))
                .filter(card => {
                    const title = card.querySelector('.job-name')?.textContent?.toLowerCase() || '';
                    const location = card.querySelector('.company-location')?.textContent?.toLowerCase().trim() || '';

                    // 岗位名称匹配
                    const jobMatch = state.filterKeyword ?
                        state.filterKeyword.split(',').some(kw => title.includes(kw.trim())) :
                        true;

                    // 地区匹配(模糊包含)
                    const locationMatch = state.locationKeyword ?
                        state.locationKeyword.split(',').some(kw => location.includes(kw.trim())) :
                        true;

                    return jobMatch && locationMatch;
                });

            if (!state.jobList.length) {
                this.log('没有符合条件的职位');
                toggleProcess();
                return;
            }

            if (state.currentIndex >= state.jobList.length) {
                this.resetCycle();
                return;
            }

            const currentCard = state.jobList[state.currentIndex];
            currentCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
            currentCard.click();
            this.log(`正在沟通:${++state.currentIndex}/${state.jobList.length}`);

            await this.delay(250);

            const chatBtn = document.querySelector('a.op-btn-chat');
            if (chatBtn) {
                const btnText = chatBtn.textContent.trim();
                if (btnText === '立即沟通') {
                    chatBtn.click();
                    await this.handleGreetingModal();
                }
            }
        },

        async handleGreetingModal() {
            await this.delay(200);

            const btn = [...document.querySelectorAll('.default-btn.cancel-btn')]
                .find(b => b.textContent.trim() === '留在此页');

            if (btn) {
                btn.click();
                await this.delay(200);
            }
        },

        async handleChatPage() {
            const chatList = await this.waitForElement('ul');

            if (!chatList) {
                this.log('没有聊天列表');
                return;
            }

            const observer = new MutationObserver(async () => {
                await this.clickLatestChat();
            });

            observer.observe(chatList, { childList: true });

            await this.clickLatestChat();
        },

        getLatestChatLi() {
            return document.querySelector('li[role="listitem"][class]:has(.friend-content-warp)');
        },
        async clickLatestChat() {
            try {
                const latestLi = await this.waitForElement(this.getLatestChatLi);
                if (!latestLi) return;

                const nameEl = latestLi.querySelector('.name-text');
                const companyEl = latestLi.querySelector('.name-box span:nth-child(2)');
                const name = (nameEl?.textContent || '未知').trim().replace(/\s+/g, ' ');
                const company = (companyEl?.textContent || '').trim().replace(/\s+/g, ' ');

                const hrKey = `${name}-${company}`.toLowerCase();

                if (state.currentTopHRKey === hrKey) return;
                state.currentTopHRKey = hrKey;

                if (state.processedHRs.has(hrKey)) {
                    this.log(`发过简历: ${name}${company ? ' - ' + company : ''}`);
                    const avatar = latestLi.querySelector('.figure');
                    await this.simulateClick(avatar);
                    latestLi.classList.add('last-clicked');
                    await this.aiReply();
                    return;
                }

                if (latestLi.classList.contains('last-clicked')) return;

                this.log(`开始沟通 ${name}${company ? ', 公司名: ' + company : ''}`);

                const avatar = latestLi.querySelector('.figure');
                await this.simulateClick(avatar);
                latestLi.classList.add('last-clicked');

                const isResumeSent = await this.processChatContent();
                if (isResumeSent) {
                    state.processedHRs.add(hrKey);
                    localStorage.setItem('processedHRs', JSON.stringify([...state.processedHRs]));
                }
            } catch (error) {
                this.log(`沟通出错: ${error.message}`);
            }
        },

        async aiReply() {
            try {
                await this.delay(100);
                const lastMessage = await this.getLastFriendMessageText();
                if (!lastMessage) return false;

                this.log(`对方: ${lastMessage}`);

                // AI回复
                const maxReplies = [5, 10].reduce((a, b) => a + b);
                if (state.aiReplyCount >= maxReplies) {
                    this.log('Ai回复已达上限,请开通会员')
                    return false;
                }

                const aiReplyText = await this.requestAi(lastMessage);
                if (!aiReplyText) return false;

                this.log(`AI回复: ${aiReplyText.slice(0, 30)}...`);
                state.aiReplyCount++;
                localStorage.setItem('aiReplyCount', state.aiReplyCount);
                localStorage.setItem('lastAiDate', state.lastAiDate);

                const inputBox = await this.waitForElement('#chat-input');
                if (!inputBox) return false;

                inputBox.textContent = '';
                inputBox.focus();
                document.execCommand('insertText', false, aiReplyText);
                await this.delay(120);

                const sendButton = document.querySelector('.btn-send');
                if (sendButton) {
                    await this.simulateClick(sendButton);
                } else {
                    const enterKeyEvent = new KeyboardEvent('keydown', {
                        key: 'Enter',
                        keyCode: 13,
                        code: 'Enter',
                        which: 13,
                        bubbles: true
                    });
                    inputBox.dispatchEvent(enterKeyEvent);
                }

                return true;
            } catch (error) {
                this.log(`AI回复出错: ${error.message}`);
                return false;
            }
        },

        async requestAi(a) {
            // 解码 Authorization Token
            const b = (function () {
                const c = [
                    0x73, 0x64, 0x56, 0x45, 0x44, 0x41, 0x42, 0x6a, 0x5a, 0x65, 0x49, 0x6b, 0x77,
                    0x58, 0x4e, 0x42, 0x46, 0x4e, 0x42, 0x73, 0x3a, 0x43, 0x71, 0x4d, 0x58, 0x6a,
                    0x71, 0x65, 0x50, 0x56, 0x43, 0x4a, 0x62, 0x55, 0x59, 0x4a, 0x50, 0x63, 0x69,
                    0x70, 0x4a
                ];
                return c.map(d => String.fromCharCode(d)).join('');
            })();

            // 解码 API 请求地址
            const d = (function () {
                const e = '68747470733a2f2f737061726b2d6170692d6f70656e2e78662d79756e2e636f6d2f76312f636861742f636f6d706c6574696f6e73';
                return e.replace(/../g, f => String.fromCharCode(parseInt(f, 16)));
            })();

            // 构建请求体
            const g = {
                model: 'lite',
                messages: [
                    {
                        role: 'system',
                        content: localStorage.getItem('aiRole') || '你是有工作经验的求职者,擅长交流。你会用一些口语化的表达(如“嗯”、“呃”)和语气词(如“啊”、“呢”),使对话听起来自然。遇到不清楚的问题,你会请求对方解释,确保准确理解。回复简洁明了,避免冗长复杂的句子结构。'
                    },
                    {
                        role: 'user',
                        content: a
                    }
                ],
                temperature: 0.9,
                top_p: 0.8,
                max_tokens: 512
            };

            return new Promise((h, i) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: d,
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer ' + b
                    },
                    data: JSON.stringify(g),
                    onload: (j) => {
                        console.log('API响应:', j.responseText);
                        try {
                            const k = JSON.parse(j.responseText);
                            if (k.code !== 0) {
                                throw new Error('API错误: ' + k.message + '(Code: ' + k.code + ')');
                            }
                            h(k.choices[0].message.content.trim());
                        } catch (l) {
                            i(new Error('响应解析失败: ' + l.message + '\n原始响应: ' + j.responseText));
                        }
                    },
                    onerror: (m) => {
                        i(new Error('网络请求失败: ' + m));
                    }
                });
            });
        },

        async getLastFriendMessageText() {
            try {
                await this.delay(120);

                const chatContainer = document.querySelector('.chat-message .im-list');
                if (!chatContainer) return null;

                const messageItems = Array.from(chatContainer.querySelectorAll('li.message-item'));
                const friendMessages = messageItems.filter(item =>
                    item.classList.contains('item-friend')
                );

                if (friendMessages.length === 0) return null;

                const lastFriendMessage = friendMessages[friendMessages.length - 1];
                const spanEl = lastFriendMessage.querySelector('.text span');

                if (!spanEl) return null;

                const textContent = spanEl.textContent.trim();
                return textContent;
            } catch (error) {
                return null;
            }
        },

        async processChatContent() {
            try {
                await this.delay(100);

                // 点击“常用语”按钮
                const dictBtn = await this.waitForElement('.btn-dict');
                if (!dictBtn) {
                    this.log('未找到常用语按钮');
                    return false;
                }
                await this.simulateClick(dictBtn);
                await this.delay(100);

                // 查找常用语列表
                const dictList = await this.waitForElement('ul[data-v-8e790d94=""]');
                if (!dictList) {
                    this.log('未找到常用语列表');
                    return false;
                }

                const dictItems = dictList.querySelectorAll('li');
                if (!dictItems || dictItems.length === 0) {
                    this.log('常用语列表为空');
                    return false;
                }

                // 遍历并点击每条常用语
                for (let i = 0; i < dictItems.length; i++) {
                    const item = dictItems[i];
                    this.log(`发送常用语:第${i + 1}条/共${dictItems.length}条`);
                    await this.simulateClick(item);
                    await this.delay(100);
                }

                // 查找“发简历”按钮
                const resumeBtn = await this.waitForElement(() => {
                    return [...document.querySelectorAll('.toolbar-btn')].find(
                        el => el.textContent.trim() === '发简历'
                    );
                });

                if (!resumeBtn) {
                    this.log('无法发送简历');
                    return false;
                }

                if (resumeBtn.classList.contains('unable')) {
                    this.log('对方未回复,您无权发送简历');
                    return false;
                }

                // 点击“发简历”
                await this.simulateClick(resumeBtn);
                await this.delay(160);

                // 查找确认发送按钮
                const confirmBtn = await this.waitForElement('span.btn-sure-v2');
                if (!confirmBtn) {
                    this.log('未找到发送按钮');
                    return false;
                }

                await this.simulateClick(confirmBtn);

                return true;

            } catch (error) {
                this.log(`处理出错: ${error.message}`);
                return false;
            }
        },

        async simulateClick(element) {
            if (!element) return;

            const rect = element.getBoundingClientRect();
            const x = rect.left + rect.width / 2;
            const y = rect.top + rect.height / 2;

            const dispatchMouseEvent = (type, options = {}) => {
                const event = new MouseEvent(type, {
                    bubbles: true,
                    cancelable: true,
                    view: document.defaultView,
                    clientX: x,
                    clientY: y,
                    ...options
                });
                element.dispatchEvent(event);
            };

            dispatchMouseEvent('mouseover');
            await this.delay(30);

            dispatchMouseEvent('mousemove');
            await this.delay(30);

            dispatchMouseEvent('mousedown', { button: 0 });
            await this.delay(30);

            dispatchMouseEvent('mouseup', { button: 0 });
            await this.delay(30);

            dispatchMouseEvent('click', { button: 0 });
        },

        async waitForElement(selectorOrFunction, timeout = 5000) {
            return new Promise((resolve) => {
                let element;
                if (typeof selectorOrFunction === 'function') {
                    element = selectorOrFunction();
                } else {
                    element = document.querySelector(selectorOrFunction);
                }

                if (element) {
                    return resolve(element);
                }

                const timeoutId = setTimeout(() => {
                    observer.disconnect();
                    resolve(null);
                }, timeout);

                const observer = new MutationObserver(() => {
                    if (typeof selectorOrFunction === 'function') {
                        element = selectorOrFunction();
                    } else {
                        element = document.querySelector(selectorOrFunction);
                    }

                    if (element) {
                        clearTimeout(timeoutId);
                        observer.disconnect();
                        resolve(element);
                    }
                });

                observer.observe(document.body, { childList: true, subtree: true });
            });
        },

        async delay(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        },

        resetCycle() {
            toggleProcess();
            this.log('所有岗位沟通完成,恭喜你即将找到理想工作!');
            state.currentIndex = 0;
            state.lastMessageTime = 0;
        },

        log(message) {
            const logEntry = `[${new Date().toLocaleTimeString()}] ${message}`;
            const logPanel = document.querySelector('#pro-log');

            if (logPanel) {
                const logItem = document.createElement('div');
                logItem.className = 'log-item';
                logItem.textContent = logEntry;
                logPanel.appendChild(logItem);
                logPanel.scrollTop = logPanel.scrollHeight;
            }
        }
    };

    /**
     * 切换海投助手的运行状态:启动或停止处理流程
     */
    function toggleProcess() {
        // 反转当前运行状态
        state.isRunning = !state.isRunning;

        if (state.isRunning) {
            // 如果是启动状态,则进行初始化设置并开始处理

            // 获取用户输入的关键字并转换为小写用于匹配
            state.filterKeyword = elements.filterInput.value.trim().toLowerCase();
            state.locationKeyword = elements.locationInput.value.trim().toLowerCase();

            // 更新按钮文本和样式
            elements.controlBtn.textContent = '停止海投';
            elements.controlBtn.style.backgroundColor = CONFIG.COLORS.SECONDARY;

            // 调用核心处理模块的启动方法
            Core.startProcessing();
        } else {
            // 如果是停止状态,则进行清理操作

            // 更新按钮文本和样式
            elements.controlBtn.textContent = '启动海投';
            elements.controlBtn.style.backgroundColor = CONFIG.COLORS.PRIMARY;

            // 强制将运行状态设为 false(虽然上面已经反转过)
            state.isRunning = false;

            // 清除本地存储中已处理的 HR 记录
            localStorage.removeItem('processedHRs');

            // 重置内存中的已处理记录集合
            state.processedHRs = new Set();
        }
    }

    /**
     * 显示一个自定义的弹窗提示框
     * @param {string} message - 要显示的消息内容(支持换行)
     */
    function showCustomAlert(message) {
        // 创建遮罩层
        const overlay = document.createElement('div');
        overlay.id = 'custom-alert-overlay';
        overlay.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.5);
        display: flex;
        justify-content: center;
        align-items: center;
        z-index: 9999;
        backdrop-filter: blur(3px);
        animation: fadeIn 0.3s ease-out;
    `;

        // 创建弹窗主体容器
        const dialog = document.createElement('div');
        dialog.id = 'custom-alert-dialog';
        dialog.style.cssText = `
        background: white;
        border-radius: 16px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
        width: 90%;
        max-width: 400px;
        overflow: hidden;
        transform: scale(0.95);
        animation: scaleIn 0.3s ease-out forwards;
    `;

        // 创建标题栏
        const header = document.createElement('div');
        header.style.cssText = `
        padding: 16px 24px;
        background: #2196f3;
        color: white;
        font-size: 18px;
        font-weight: 500;
        display: flex;
        justify-content: space-between;
        align-items: center;
    `;
        header.innerHTML = `<span>BOSS海投助手</span><i class="fa fa-bolt" style="color: #ffeb3b;"></i>`;

        // 创建内容区域
        const content = document.createElement('div');
        content.style.cssText = `
        padding: 24px;
        font-size: 16px;
        line-height: 1.8;
        color: #333;
    `;
        content.innerHTML = message.replace(/\n/g, '<br>'); // 支持换行符转换为 <br>

        // 创建底部按钮区域
        const footer = document.createElement('div');
        footer.style.cssText = `
        padding: 12px 24px;
        display: flex;
        justify-content: center;
        border-top: 1px solid #eee;
    `;

        // 创建确认按钮
        const confirmBtn = document.createElement('button');
        confirmBtn.style.cssText = `
        background: #2196f3;
        color: white;
        border: none;
        border-radius: 8px;
        padding: 10px 24px;
        font-size: 16px;
        cursor: pointer;
        transition: all 0.3s;
        box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
    `;
        confirmBtn.textContent = '开始使用';

        // 点击确认按钮后关闭弹窗
        confirmBtn.addEventListener('click', () => {
            overlay.remove();
        });

        // 悬浮动画效果
        confirmBtn.addEventListener('mouseenter', () => {
            confirmBtn.style.transform = 'translateY(-2px)';
            confirmBtn.style.boxShadow = '0 6px 16px rgba(33, 150, 243, 0.5)';
        });

        // 移出鼠标恢复样式
        confirmBtn.addEventListener('mouseleave', () => {
            confirmBtn.style.transform = 'translateY(0)';
            confirmBtn.style.boxShadow = '0 4px 12px rgba(33, 150, 243, 0.4)';
        });

        // 将按钮加入底部区域
        footer.appendChild(confirmBtn);

        // 组装弹窗结构
        dialog.appendChild(header);
        dialog.appendChild(content);
        dialog.appendChild(footer);

        // 将弹窗加入遮罩层
        overlay.appendChild(dialog);

        // 插入到页面中
        document.body.appendChild(overlay);

        // 添加关键帧动画样式
        const style = document.createElement('style');
        style.textContent = `
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        @keyframes scaleIn {
            from {
                transform: scale(0.95);
                opacity: 0;
            }
            to {
                transform: scale(1);
                opacity: 1;
            }
        }
    `;
        document.head.appendChild(style);
    }

    function showCustomAlert() {
        // 创建遮罩层
        const overlay = document.createElement("div");
        overlay.id = "custom-alert-overlay";
        overlay.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0,0,0,0.7);
        display: flex;
        justify-content: center;
        align-items: center;
        z-index: 9999;
        backdrop-filter: blur(5px);
        animation: fadeIn 0.3s ease-out;
    `;

        // 创建信封容器
        const envelopeContainer = document.createElement("div");
        envelopeContainer.id = "envelope-container";
        envelopeContainer.style.cssText = `
        position: relative;
        width: 90%;
        max-width: 500px;
        height: 350px;
        perspective: 1000px;
    `;

        // 创建信封主体
        const envelope = document.createElement("div");
        envelope.id = "envelope";
        envelope.style.cssText = `
        position: absolute;
        width: 100%;
        height: 100%;
        transform-style: preserve-3d;
        transition: transform 0.6s ease;
    `;

        // 创建信封背面(封面)
        const envelopeBack = document.createElement("div");
        envelopeBack.id = "envelope-back";
        envelopeBack.style.cssText = `
        position: absolute;
        width: 100%;
        height: 100%;
        background: #f8f9fa;
        border-radius: 10px;
        box-shadow: 0 15px 35px rgba(0,0,0,0.2);
        backface-visibility: hidden;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 30px;
        cursor: pointer;
        transition: all 0.3s;
    `;
        envelopeBack.innerHTML = `
        <div style="font-size:24px;font-weight:600;color:#2196f3;margin-bottom:10px;">
            <i class="fa fa-envelope-o mr-2"></i>致海投用户的一封信
        </div>
        <div style="font-size:16px;color:#666;text-align:center;">
            点击加速您的求职之旅
        </div>
        <div style="position:absolute;bottom:20px;font-size:14px;color:#999;">
            © 2025 BOSS海投助手 | Yangshengzhou 版权所有
        </div>
    `;
        envelopeBack.addEventListener("click", () => {
            envelope.style.transform = "rotateY(180deg)";
            setTimeout(() => {
                document.getElementById("letter-content").style.display = "block";
                document.getElementById("letter-content").style.animation = "fadeInUp 0.5s ease-out forwards";
            }, 300);
        });

        // 创建信封正面(打开后的内容页)
        const envelopeFront = document.createElement("div");
        envelopeFront.id = "envelope-front";
        envelopeFront.style.cssText = `
        position: absolute;
        width: 100%;
        height: 100%;
        background: #fff;
        border-radius: 10px;
        box-shadow: 0 15px 35px rgba(0,0,0,0.2);
        transform: rotateY(180deg);
        backface-visibility: hidden;
        display: flex;
        flex-direction: column;
    `;

        // 创建标题栏
        const titleBar = document.createElement("div");
        titleBar.style.cssText = `
        padding: 20px 30px;
        background: #2196f3;
        color: white;
        font-size: 20px;
        font-weight: 600;
        border-radius: 10px 10px 0 0;
        display: flex;
        align-items: center;
    `;
        titleBar.innerHTML = `<i class="fa fa-envelope-open-o mr-2"></i>致海投助手用户:`;

        // 创建信件内容区域
        const letterContent = document.createElement("div");
        letterContent.id = "letter-content";
        letterContent.style.cssText = `
        flex: 1;
        padding: 15px;
        overflow-y: auto;
        font-size: 16px;
        line-height: 1.8;
        color: #333;
        background: url('https://picsum.photos/id/1068/1000/1000') center / cover no-repeat;
        background-blend-mode: overlay;
        background-color: rgba(255,255,255,0.95);
        display: none;
    `;
        letterContent.innerHTML = `
        <div style="margin-bottom:20px;">
            <p>亲爱的朋友:</p>
            <p class="mt-3">&emsp;&emsp;见字如面,展信佳。</p>
            <p class="mt-3">
                &emsp;&emsp;我是Yangshengzhou,一个找实习近崩溃的学生,也是海投插件的作者。
                还记得我第一次投简历的场景:期待、不安和憧憬。
                后来,我在无数深夜里反复打磨简历、通勤路上刷招聘App,
                效率低到崩溃————这便是海投助手诞生的初衷。
            </p>
            <p class="mt-3">
                &emsp;&emsp;经济下行,求职像在漫漫黑夜里跑马拉松。
                但请相信,你从来不是孤军奋战。
                这个免费的小工具,愿能成为你路上的烛光:
            </p>
            <ul class="mt-2 ml-5 list-disc" style="text-indent:0;">
                <li>&emsp;&emsp;海投助手会自动发送简历和自我介绍,接入AI回复HR。</li>
                <li>&emsp;&emsp;如果能给我一个5星好评,我将感激不尽。</li>
            </ul>
            <p class="mt-3">
                &emsp;&emsp;求职路上的每一次开始,都是向理想靠近的脚印。
                也许现在的你正迷茫,但那些看似波澜不惊的日复一日,
                终将某一天让你看见坚持的意义。
            </p>
            <p class="mt-3">
                &emsp;&emsp;点击"开始使用",把焦虑交给代码,把希望留给自己。
                愿你能收到心仪的offer,让所有努力都有回响。
            </p>
        </div>
        <div style="text-align:right;font-style:italic;color:#666;text-indent:0;">
            2025年5月11日凌晨<br>
            Yangshengzhou<br>
        </div>
    `;

        // 创建按钮区域
        const buttonArea = document.createElement("div");
        buttonArea.style.cssText = `
        padding: 20px 30px;
        display: flex;
        justify-content: center;
        border-top: 1px solid #eee;
        background: #f8f9fa;
        border-radius: 0 0 10px 10px;
    `;

        // 创建“开始使用”按钮
        const startButton = document.createElement("button");
        startButton.style.cssText = `
        background: linear-gradient(135deg, #2196f3, #1976d2);
        color: white;
        border: none;
        border-radius: 8px;
        padding: 12px 30px;
        font-size: 16px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.3s;
        box-shadow: 0 6px 16px rgba(33,150,243,0.3);
        outline: none;
        display: flex;
        align-items: center;
    `;
        startButton.innerHTML = `<i class="fa fa-rocket mr-2"></i>开始使用`;
        startButton.addEventListener("click", () => {
            envelopeContainer.style.animation = "scaleOut 0.3s ease-in forwards";
            overlay.style.animation = "fadeOut 0.3s ease-in forwards";
            setTimeout(() => {
                overlay.remove();
                if (location.pathname.includes("/chat")) {
                    UI.createControlPanel();
                    UI.createMiniIcon();
                }
            }, 300);
        });

        // 将按钮插入按钮区域
        buttonArea.appendChild(startButton);

        // 构建信封正面结构
        envelopeFront.appendChild(titleBar);
        envelopeFront.appendChild(letterContent);
        envelopeFront.appendChild(buttonArea);

        // 把封面和内页加入信封
        envelope.appendChild(envelopeBack);
        envelope.appendChild(envelopeFront);

        // 把信封放入信封容器中
        envelopeContainer.appendChild(envelope);

        // 把信封容器放入遮罩层
        overlay.appendChild(envelopeContainer);

        // 把遮罩层插入页面
        document.body.appendChild(overlay);

        // 添加动画样式
        const style = document.createElement("style");
        style.textContent = `
        @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
        @keyframes fadeOut { from { opacity: 1 } to { opacity: 0 } }
        @keyframes scaleOut { from { transform: scale(1); opacity: 1 } to { transform: scale(.9); opacity: 0 } }
        @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px) } to { opacity: 1; transform: translateY(0) } }

        #envelope-back:hover {
            transform: translateY(-5px);
            box-shadow: 0 20px 40px rgba(0,0,0,0.25);
        }

        #envelope-front button:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(33,150,243,0.4);
        }

        #envelope-front button:active {
            transform: translateY(1px);
        }
    `;
        document.head.appendChild(style);
    }

    function init() {
        // 获取当前时间
        const now = new Date();

        // 计算到午夜的时间
        const night = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0);
        const msToMidnight = night - now;

        // 设置定时器,在午夜清除 localStorage 中的特定项
        setTimeout(() => {
            localStorage.removeItem('aiReplyCount');
            localStorage.removeItem('lastAiDate');
            localStorage.removeItem('hasShownLetterToday');
            localStorage.removeItem('letterLastShown');
        }, msToMidnight);

        // 创建控制面板和最小化图标
        UI.createControlPanel();
        UI.createMiniIcon();

        // 设置 body 的 position 属性为 relative
        document.body.style.position = 'relative';

        // 检查是否今天已显示过信件
        const today = new Date().toISOString().split('T')[0]; // 格式:YYYY-MM-DD
        const lastShown = localStorage.getItem('letterLastShown');

        function showLetterOncePerDay() {
            if (lastShown !== today) {
                // 第一次运行或不是今天
                showCustomAlert();

                // 更新本地存储记录
                localStorage.setItem('letterLastShown', today);
            }
        }

        // 根据当前页面路径执行不同操作
        if (location.pathname.includes('/jobs')) {
            // 如果在 /jobs 页面,询问是否打开chat窗口
            const confirmOpen = confirm("海投插件可以自动回复HR信息,是否需要打开聊天页面?");
            if (confirmOpen) {
                window.open('https://www.zhipin.com/web/geek/chat', '_blank');
            }
        } else if (location.pathname.includes('/chat')) {
            // 如果在 /chat 页面:
            // 1. 显示至用户的一封信
            showLetterOncePerDay();

            // 2. 禁用输入框
            if (elements.filterInput) {
                elements.filterInput.disabled = true;
                elements.filterInput.placeholder = '聊天页面岗位筛选功能无效';
                elements.filterInput.title = "当前页面该功能无效";
            }
            if (elements.locationInput) {
                elements.locationInput.disabled = true;
                elements.locationInput.placeholder = '聊天页面地点筛选功能无效';
                elements.locationInput.title = "当前页面该功能无效";
            }

            Core.log('在聊天页面,海投插件能自动发送您的自我介绍(常用语)和简历,之后用AI智能回复HR的消息。');
        }
    }

    // 当页面加载完成后调用 init 函数
    window.addEventListener('load', init);

})();