Boss Batch Push [Boss直聘批量投简历]

boss直聘批量简历投递

当前为 2023-06-07 提交的版本,查看 最新版本

// ==UserScript==
// @name         Boss Batch Push [Boss直聘批量投简历]
// @description  boss直聘批量简历投递
// @namespace    maple
// @version      1.0.0
// @author       maple.
// @license      Apache License 2.0
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @match        https://www.zhipin.com/*
// ==/UserScript==


const docTextArr = [
    "1.批量投递:点击批量投递开始批量投简历,请先通过上方Boss的筛选功能筛选大致的范围,然后通过脚本的筛选进一步确认投递目标。",
    "2.重置开关:如果你需要自己浏览工作详情页面,请点击该按钮关闭自动投递。如果不关闭,打开工作详情页,会自动投递并关闭页面。",
    "3.保存配置:保持下方脚本筛选项,用于后续直接使用当前配置。",
    "4.过滤不活跃Boss:打开后会自动过滤掉最近未活跃的Boss发布的工作。以免浪费每天的100次机会。",
    "脚本筛选项介绍:",
    "公司名包含:投递工作的公司名一定包含在当前集合中,模糊匹配,多个使用逗号分割。这个一般不用,如果使用了也就代表只投这些公司的岗位。例子:【阿里,华为】",
    "排除公司名:投递工作的公司名一定不在当前集合中,也就是排除当前集合中的公司,模糊匹配,多个使用逗号分割。例子:【xxx外包】",
    "Job名包含:投递工作的名称一定包含在当前集合中,模糊匹配,多个使用逗号分割。例如:【软件,Java,后端,服务端,开发,后台】",
    "薪资范围:投递工作的薪资范围一定在当前区间中,一定是区间,使用-连接范围。例如:【12-20】",
    "公司规模范围:投递工作的公司人员范围一定在当前区间中,一定是区间,使用-连接范围。例如:【500-20000000】",
]


/**
 * 以下名称均为模糊匹配
 * @companyArr: 公司名
 * @companyExclude: 排除公司名
 * @jobNameArr: job名
 * @salaryRange: 薪资范围
 * @companyScale: 公司规模范围
 *
 */
let companyArr = [];
let companyExclude = [];
let jobNameArr = [];
let salaryRange = "";
let companyScale = "";


/**
 * 投递多少页,每页默认有30个job,筛选过后不确定
 * @type {number}
 */
const pushPageCount = 100;


/**
 * 当前页,勿动!
 * @type {number}
 */
let currentPage = 0;

/**
 * 本地存储key
 */
const ACTIVE_READY = "activeReady";
const ACTIVE_ENABLE = "activeEnable";
const LOCAL_CONFIG = "config";
const PUSH_COUNT = "pushCount";
const PUSH_LOCK = "lock";
const PUSH_LIMIT = "limit";
const BATCH_ENABLE = "enable";


(function () {
    'use strict';

    let loadConfig;
    let saveConfig;

    /**
     * jobList页面处理逻辑
     * 添加操作按钮,注册点击事件,加载持久化配置
     */
    const jobListHandler = () => {

        // 重置逻辑状态,可能由于执行过程的中断导致状态错乱
        resetStatus()

        // 批量投递按钮
        const batchButton = document.createElement('button');
        batchButton.innerText = '批量投递';
        batchButton.addEventListener('click', batchHandler);

        // 重置开关按钮
        const resetButton = document.createElement('button');
        resetButton.innerText = '重置开关';
        resetButton.addEventListener('click', () => {
            GM_setValue(BATCH_ENABLE, false)
            console.log("重置脚本开关成功")
            window.alert("重置脚本开关成功")
        });

        // 保存配置按钮
        const saveButton = document.createElement('button');
        saveButton.innerText = '保存配置';
        saveButton.addEventListener('click', () => {
            saveConfig();
            window.alert("保存配置成功")
        });

        // 过滤不活跃boss按钮
        const switchButton = document.createElement('button');


        const addStyle = (button) => {
            button.style.display = "inline-block";
            button.style.padding = "10px 20px";
            button.style.borderRadius = "5px";
            button.style.backgroundColor = "#409eff";
            button.style.color = "#ffffff";
            button.style.textDecoration = "none";
            button.style.margin = "10px";
        }
        addStyle(batchButton)
        addStyle(resetButton)
        addStyle(saveButton)
        addStyle(switchButton)

        let switchState = false;
        const setSwitchButtonState = (isOpen) => {
            switchState = isOpen;
            if (isOpen) {
                switchButton.innerText = '过滤不活跃Boss:已开启';
                switchButton.style.backgroundColor = '#67c23a';
                GM_setValue(ACTIVE_ENABLE, true)
            } else {
                switchButton.innerText = '过滤不活跃Boss:已关闭';
                switchButton.style.backgroundColor = '#f56c6c';
                GM_setValue(ACTIVE_ENABLE, false)
            }
        };
        setSwitchButtonState(GM_getValue(ACTIVE_ENABLE, true))

        // 添加事件监听,执行回调函数
        switchButton.addEventListener('click', () => {
            setSwitchButtonState(!switchState);
        });

        // 等待页面元素渲染,然后加载配置并渲染页面
        setTimeout(() => {
            // 读取配置
            initConfig()
            const container = document.querySelector('.job-list-wrapper');
            const firstJob = container.firstElementChild;
            container.insertBefore(addDocComponent(), firstJob);
            container.insertBefore(batchButton, firstJob);
            container.insertBefore(resetButton, firstJob);
            container.insertBefore(saveButton, firstJob);
            container.insertBefore(switchButton, firstJob);
        }, 1000)
    };

    const addDocComponent = () => {
        const docDiv = document.createElement("div");
        docDiv.style.backgroundColor = "#f2f2f2";
        docDiv.style.padding = "5px";
        docDiv.style.width = "100%";
        for (let i = 0; i < docTextArr.length; i++) {
            const textTag = document.createElement("p");
            textTag.style.color = "#666";
            textTag.innerHTML = docTextArr[i];
            docDiv.appendChild(textTag)
        }

        return docDiv;
    }

    const resetStatus = () => {
        GM_setValue(PUSH_COUNT, 0)
        GM_setValue(PUSH_LIMIT, false)
    }

    const initConfig = () => {

        // 加载持久化的配置,并加载到内存
        const config = JSON.parse(GM_getValue(LOCAL_CONFIG, "{}"))
        companyArr = companyArr.concat(config.companyArr)
        companyExclude = companyExclude.concat(config.companyExclude)
        jobNameArr = jobNameArr.concat(config.jobNameArr)
        salaryRange = config.salaryRange ? config.salaryRange : salaryRange
        companyScale = config.companyScale ? config.companyScale : companyScale

        // 将配置渲染到页面
        renderConfigText()

        /**
         * 渲染配置输入框
         * 将用户配置渲染到页面
         * 同时将钩子函数赋值!!!
         */
        function renderConfigText() {
            const bossInput = document.createElement("div");
            bossInput.id = "boss-input";

            const companyLabel1 = document.createElement("label");
            companyLabel1.textContent = "公司名包含";
            const companyArr_ = document.createElement("input");
            companyArr_.type = "text";
            companyArr_.id = "companyArr";
            companyLabel1.appendChild(companyArr_);
            bossInput.appendChild(companyLabel1);
            companyArr_.value = deWeight(companyArr).join(",")

            const companyLabel2 = document.createElement("label");
            companyLabel2.textContent = "公司名排除";
            const companyExclude_ = document.createElement("input");
            companyExclude_.type = "text";
            companyExclude_.id = "companyExclude";
            companyLabel2.appendChild(companyExclude_);
            bossInput.appendChild(companyLabel2);
            companyExclude_.value = deWeight(companyExclude).join(",")

            const jobNameLabel = document.createElement("label");
            jobNameLabel.textContent = "Job名包含";
            const jobNameArr_ = document.createElement("input");
            jobNameArr_.type = "text";
            jobNameArr_.id = "jobNameArr";
            jobNameLabel.appendChild(jobNameArr_);
            bossInput.appendChild(jobNameLabel);
            jobNameArr_.value = deWeight(jobNameArr).join(",")

            const salaryLabel = document.createElement("label");
            salaryLabel.textContent = "薪资范围";
            const salaryRange_ = document.createElement("input");
            salaryRange_.type = "text";
            salaryRange_.id = "salaryRange";
            salaryLabel.appendChild(salaryRange_);
            bossInput.appendChild(salaryLabel);
            salaryRange_.value = salaryRange

            const companyScaleLabel = document.createElement("label");
            companyScaleLabel.textContent = "公司规模范围";
            const companyScale_ = document.createElement("input");
            companyScale_.type = "text";
            companyScale_.id = "companyScale";
            companyScaleLabel.appendChild(companyScale_);
            bossInput.appendChild(companyScaleLabel);
            companyScale_.value = companyScale

            // 美化样式
            bossInput.style.margin = "20px";
            bossInput.style.padding = "20px";
            bossInput.style.border = "1px solid #ccc";
            bossInput.style.background = "#f0f0f0";
            bossInput.style.borderRadius = "10px";
            bossInput.style.width = "80%";

            const labels = bossInput.querySelectorAll("label");
            labels.forEach(label => {
                label.style.display = "inline-block";
                label.style.width = "20%";
                label.style.fontWeight = "bold";
            });

            const inputs = bossInput.querySelectorAll("input[type='text']");
            inputs.forEach(input => {
                input.style.marginLeft = "10px";
                input.style.width = "70%";
                input.style.padding = "5px";
                input.style.borderRadius = "5px";
                input.style.border = "1px solid #ccc";
                input.style.boxSizing = "border-box";
            });

            // 将创建好的元素添加到 DOM 中
            const container = document.querySelector('.job-list-wrapper');
            const firstJob = container.firstElementChild;
            container.insertBefore(bossInput, firstJob);

            /**
             * 每次批量提交前,加载页面最新的配置
             */
            loadConfig = () => {
                companyArr = companyArr_.value.split(",")
                companyExclude = companyExclude_.value.split(",")
                jobNameArr = jobNameArr_.value.split(",")
                salaryRange = salaryRange_.value
                companyScale = companyScale_.value = companyScale
            }

            /**
             * 持久化用户输入的配置
             */
            saveConfig = () => {
                const config = {
                    companyArr: companyArr_.value.split(","),
                    companyExclude: companyExclude_.value.split(","),
                    jobNameArr: jobNameArr_.value.split(","),
                    salaryRange: salaryRange_.value,
                    companyScale: companyScale_.value = companyScale,
                }
                // 持久化配置
                GM_setValue(LOCAL_CONFIG, JSON.stringify(config))
            }
        }

        function deWeight(arr) {
            let uniqueArr = [];
            for (let i = 0; i < arr.length; i++) {
                if (uniqueArr.indexOf(arr[i]) === -1) {
                    uniqueArr.push(arr[i]);
                }
            }
            return uniqueArr;
        }
    }


    /**
     * 详情页面注册load事件
     * 点击job详情的立即沟通
     */
    const jobDetailHandler = () => {
        if (!GM_getValue(BATCH_ENABLE, false)) {
            console.log("未开启脚本开关")
            return;
        }

        /**
         * 招聘boss是否活跃
         */
        const isBossActive = () => {
            const activeEle = document.querySelector(".boss-active-time")
            if (!activeEle) {
                return true;
            }

            const activeText = activeEle.innerText;
            return !(activeText.includes("月") || activeText.includes("年"));
        }

        // 关闭页面并重置对应状态
        const closeTab = (ms) => {
            console.log("关闭页面")
            setTimeout(() => {
                // 沟通限制对话框
                const limitDialog = document.querySelector(".dialog-container");
                if (limitDialog && !limitDialog.innerText.includes("已向BOSS发送消息")) {
                    GM_setValue(PUSH_LIMIT, true)
                }
                GM_setValue(PUSH_LOCK, false)
                window.close()
            }, ms)
        }

        // boss是否活跃,过滤不活跃boss
        if (!isBossActive()) {
            console.log("过滤不活跃boss")
            closeTab(0)
            return;
        }

        // 立即沟通或者继续沟通按钮
        const handlerButton = document.querySelector(".btn-startchat");
        if (handlerButton.innerText.includes("立即沟通")) {
            // 如果是沟通按钮则点击
            console.log("点击立即沟通")
            handlerButton.click()
            // 更新投递次数,可能存在性能问题
            GM_setValue(PUSH_COUNT, GM_getValue(PUSH_COUNT, 0) + 1)
        }

        closeTab(300)
    }

    /**
     * 批量操作按钮注册的事件
     * 用于批量打开job详情
     */
    const batchHandler = () => {
        // 每次投递加载最新的配置
        loadConfig();
        console.log("开始批量投递,当前页数:", ++currentPage)
        GM_setValue(BATCH_ENABLE, true)

        async function clickJobList(jobList, delay) {
            // 过滤只留下立即沟通的job
            jobList = filterJob(jobList);
            await activeWait()
            console.log("过滤后的job数量", jobList.length, "默认30")
            for (let i = 0; i < jobList.length; i++) {
                const job = jobList[i];
                let innerText = job.querySelector(".job-title").innerText;
                const jobTitle = innerText.replace("\n", " ");

                const lock = GM_getValue(PUSH_LOCK, false)
                while (true) {
                    if (!lock) {
                        console.log("解锁---" + jobTitle)
                        break;
                    }
                    console.log("等待---" + jobTitle)
                    // 每500毫秒检查一次状态
                    await sleep(500);
                }

                if (GM_getValue(PUSH_LIMIT, false)) {
                    console.log("今日沟通已达boss限制")
                    break;
                }

                // 当前table页是活跃的,也是另外一遍点击立即沟通之后,以及关闭页面
                await new Promise(resolve => setTimeout(resolve, delay));
                GM_setValue(PUSH_LOCK, false)
                console.log("加锁---" + jobTitle)
                job.click();
            }

            if (currentPage >= pushPageCount || GM_getValue(PUSH_LIMIT, false)) {
                console.log("一共", pushPageCount, "页")
                console.log("共投递", GM_getValue(PUSH_COUNT, 0), "份")
                console.log("投递完毕")
                clear()
                return;
            }

            const nextButton = document.querySelector(".ui-icon-arrow-right")
            // 没有下一页
            if (nextButton.parentElement.className === "disabled") {
                window.alert("共投递" + GM_getValue(PUSH_COUNT, 0) + "份,没有更多符合条件的工作")
                clear();
                return;
            }

            console.log("下一页")
            nextButton.click()
            setTimeout(() => batchHandler(), 2000);

        }

        // 每隔2秒执行一次点击操作
        clickJobList(document.querySelectorAll('.job-card-wrapper'), 2000);
    };

    async function activeWait() {
        // 未开启活跃度检查
        if (!GM_getValue(ACTIVE_ENABLE, false)) {
            return new Promise(resolve => resolve())
        }
        return new Promise((resolve) => {
            const timer = setInterval(() => {
                if (GM_getValue(ACTIVE_ENABLE, false) && GM_getValue(ACTIVE_READY, false)) {
                    clearInterval(timer);
                    resolve();
                }
                console.log("阻塞中---------", GM_getValue(ACTIVE_ENABLE, false), GM_getValue(ACTIVE_READY, false))
            }, 1000);
        });
    }

    function clear() {
        GM_setValue(PUSH_LOCK, false)
        GM_setValue(PUSH_LIMIT, false)
        GM_setValue(BATCH_ENABLE, false)
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * 过滤job【去除已经沟通过的job】
     * @param job_list
     * @returns {*[]}
     */
    const filterJob = (job_list) => {
        const result = [];
        let requestCount = 0;
        for (let i = 0; i < job_list.length; i++) {
            let job = job_list[i];
            let innerText = job.querySelector(".job-title").innerText;
            const jobTitle = innerText.replace("\n", " ");

            // 匹配符合条件的Job
            if (!matchJob(job)) {
                console.log("跳过不匹配的job:", jobTitle)
                continue;
            }

            const jobStatusStr = job.querySelector(".start-chat-btn").innerText;
            if (!jobStatusStr.includes("立即沟通")) {
                continue;
            }

            // 打开boss活跃度开关时,需要检查boss活跃度
            if (!GM_getValue(ACTIVE_ENABLE, false)) {
                // 未打开boss活跃度开关
                result.push(job);
                continue
            }

            // 活跃度检查【如果是活跃才添加到result中】
            requestCount++;
            const params = job.querySelector(".job-card-left").href.split("?")[1]
            axios.get("https://www.zhipin.com/wapi/zpgeek/job/card.json?" + params).then(resp => {
                const activeText = resp.data.zpData.jobCard.activeTimeDesc
                if ((activeText.includes("月") || activeText.includes("年"))) {
                    console.log("过滤不活跃bossJob:" + jobTitle)
                    return
                }
                console.log("添加活跃bossJob:" + jobTitle)
                result.push(job);
            }).catch(e => {
                console.log(e)
            }).finally(() => {
                requestCount--;
                if (requestCount === 0) {
                    GM_setValue(ACTIVE_READY, true)
                }
            })
        }
        return result;
    }


    /**
     * 匹配job
     * 返回true表示当前job匹配,命中
     *
     * @param job
     * @returns {boolean}
     */
    const matchJob = (job) => {
        // 公司名
        const companyName = job.querySelector(".company-name").innerText
        // 工作名
        const jobName = job.querySelector(".job-name").innerText
        // 新增范围
        const salary = job.querySelector(".salary").innerText
        // 公司规模范围
        const companyScale_ = job.querySelector(".company-tag-list").lastChild.innerText

        // 模糊匹配
        const companyNameCondition = fuzzyMatch(companyArr, companyName, true)
        const companyNameExclude = fuzzyMatch(companyExclude, companyName, true)
        const jobNameCondition = fuzzyMatch(jobNameArr, jobName, true)

        // 范围匹配
        const salaryRangeCondition = rangeMatch(salaryRange, salary)
        const companyScaleCondition = rangeMatch(companyScale, companyScale_)

        return companyNameCondition && !companyNameExclude && jobNameCondition && salaryRangeCondition && companyScaleCondition;

    }

    /**
     * 匹配范围
     * @param rangeStr
     * @param input
     * @returns {boolean}
     */
    function rangeMatch(rangeStr, input) {
        if (!rangeStr) {
            return true;
        }
        // 匹配定义范围的正则表达式
        let reg = /^(\d+)(?:-(\d+))?$/;
        let match = rangeStr.match(reg);

        if (match) {
            let start = parseInt(match[1]);
            let end = parseInt(match[2] || match[1]);

            // 如果输入只有一个数字的情况
            if (/^\d+$/.test(input)) {
                let number = parseInt(input);
                return number >= start && number <= end;
            }

            // 如果输入有两个数字的情况
            let inputReg = /^(\d+)(?:-(\d+))?/;
            let inputMatch = input.match(inputReg);
            if (inputMatch) {
                let inputStart = parseInt(inputMatch[1]);
                let inputEnd = parseInt(inputMatch[2] || inputMatch[1]);
                return (inputStart >= start && inputStart <= end) || (inputEnd >= start && inputEnd <= end);
            }
        }

        // 其他情况均视为不匹配
        return false;
    }

    function fuzzyMatch(arr, input, emptyStatus) {
        if (arr.length === 0) {
            // 为空时直接返回指定的空状态
            return emptyStatus;
        }
        input = input.toLowerCase();
        let emptyEle = false;
        // 遍历数组中的每个元素
        for (let i = 0; i < arr.length; i++) {
            // 如果当前元素包含指定值,则返回 true
            let arrEleStr = arr[i].toLowerCase();
            if (arrEleStr.length === 0) {
                emptyEle = true;
                continue;
            }
            if (arrEleStr.includes(input) || input.includes(arrEleStr)) {
                return true;
            }
        }

        // 所有元素均为空元素【返回空状态】
        if (emptyEle) {
            return emptyStatus;
        }

        // 如果没有找到匹配的元素,则返回 false
        return false;
    }


    /**
     * 主启动方法
     * 根据不同的url注册不同的事件
     */
    function main() {

        const list_url = "web/geek/job";
        const detail_url = "job_detail";

        if (document.URL.includes(list_url)) {
            window.addEventListener('load', jobListHandler);
        } else if (document.URL.includes(detail_url)) {
            window.addEventListener('load', jobDetailHandler);
        }
    }

    main();

})();