// ==UserScript== // @name 忒星boss直聘批量简历投递+自动发送自定义消息[忒星修复魔改版] // @description 忒星boss直聘批量简历投递[忒星修复魔改版] // @namespace yongjiu // @version 1.2.4 // @author maple,Ocyss,忒星 // @license Apache License 2.0 // @run-at document-start // @match https://www.zhipin.com/* // @connect https://github.com/yongjiu8/boss_push // @include https://www.zhipin.com // @require https://unpkg.com/maple-lib@1.0.3/log.js // @require https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js // @require https://cdn.jsdelivr.net/npm/js2wordcloud@1.1.12/dist/js2wordcloud.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_addValueChangeListener // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_cookie // @grant GM_notification // @downloadURL https://update.greasyfork.icu/scripts/481022/%E5%BF%92%E6%98%9Fboss%E7%9B%B4%E8%81%98%E6%89%B9%E9%87%8F%E7%AE%80%E5%8E%86%E6%8A%95%E9%80%92%2B%E8%87%AA%E5%8A%A8%E5%8F%91%E9%80%81%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B6%88%E6%81%AF%5B%E5%BF%92%E6%98%9F%E4%BF%AE%E5%A4%8D%E9%AD%94%E6%94%B9%E7%89%88%5D.user.js // @updateURL https://update.greasyfork.icu/scripts/481022/%E5%BF%92%E6%98%9Fboss%E7%9B%B4%E8%81%98%E6%89%B9%E9%87%8F%E7%AE%80%E5%8E%86%E6%8A%95%E9%80%92%2B%E8%87%AA%E5%8A%A8%E5%8F%91%E9%80%81%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B6%88%E6%81%AF%5B%E5%BF%92%E6%98%9F%E4%BF%AE%E5%A4%8D%E9%AD%94%E6%94%B9%E7%89%88%5D.meta.js // ==/UserScript== "use strict"; let logger = Logger.log("info") class BossBatchExp extends Error { constructor(msg) { super(msg); this.name = "BossBatchExp"; } } class JobNotMatchExp extends BossBatchExp { constructor(msg) { super(msg); this.name = "JobNotMatchExp"; } } class PublishLimitExp extends BossBatchExp { constructor(msg) { super(msg); this.name = "PublishLimitExp"; } } class FetchJobDetailFailExp extends BossBatchExp { constructor(msg) { super(msg); this.name = "FetchJobDetailFailExp"; } } class SendPublishExp extends BossBatchExp { constructor(msg) { super(msg); this.name = "SendPublishExp"; } } class PublishStopExp extends BossBatchExp { constructor(msg) { super(msg); this.name = "PublishStopExp"; } } class TampermonkeyApi { static CUR_CK = "" constructor() { // fix 还未创建对象时,CUR_CK为空字符串,创建完对象之后【如果没有配置,则为null】导致key前缀不一致 TampermonkeyApi.CUR_CK = GM_getValue("ck_cur", ""); } static GmSetValue(key, val) { return GM_setValue(TampermonkeyApi.CUR_CK + key, val); } static GmGetValue(key, defVal) { return GM_getValue(TampermonkeyApi.CUR_CK + key, defVal); } static GMXmlHttpRequest(options) { return GM_xmlhttpRequest(options) } static GmAddValueChangeListener(key, func) { return GM_addValueChangeListener(TampermonkeyApi.CUR_CK + key, func); } static GmNotification(content) { GM_notification({ title: "Boss直聘批量投简历", image: "https://img.bosszhipin.com/beijin/mcs/banner/3e9d37e9effaa2b6daf43f3f03f7cb15cfcd208495d565ef66e7dff9f98764da.jpg", text: content, highlight: true, // 布尔值,是否突出显示发送通知的选项卡 silent: true, // 布尔值,是否播放声音 timeout: 10000, // 设置通知隐藏时间 onclick: function () { console.log("点击了通知"); }, ondone() { }, // 在通知关闭(无论这是由超时还是单击触发)或突出显示选项卡时调用 }); } } class Tools { /** * 模糊匹配 * @param arr * @param input * @param emptyStatus * @returns {boolean|*} */ static 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; } // 范围匹配 static rangeMatch(rangeStr, input, by = 1) { if (!rangeStr) { return true; } // 匹配定义范围的正则表达式 let reg = /^(\d+)(?:-(\d+))?$/; let match = rangeStr.match(reg); if (match) { let start = parseInt(match[1]) * by; let end = parseInt(match[2] || match[1]) * by; // 如果输入只有一个数字的情况 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; } /** * 语义匹配 * @param configArr * @param content * @returns {boolean} */ static semanticMatch(configArr, content) { for (let i = 0; i < configArr.length; i++) { if (!configArr[i]) { continue } let re = new RegExp("(? `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); return `${baseURL}?${queryString}`; } } class DOMApi { static createTag(tag, name, style) { let htmlTag = document.createElement(tag); if (name) { htmlTag.innerHTML = name; } if (style) { htmlTag.style.cssText = style; } return htmlTag; } static createInputTag(descName, valueStr) { const inputNameLabel = document.createElement("label"); inputNameLabel.textContent = descName; const inputTag = document.createElement("input"); inputTag.type = "text"; inputNameLabel.appendChild(inputTag); if (valueStr) { inputTag.value = valueStr; } // 样式 inputNameLabel.style.cssText = "display: inline-block; margin: 0px 10px; font-weight: bold; width: 200px;"; inputTag.style.cssText = "margin-left: 2px; width: 100%; padding: 5px; border-radius: 5px; border: 1px solid rgb(204, 204, 204); box-sizing: border-box;"; return inputNameLabel; } static getInputVal(inputLab) { return inputLab.querySelector("input").value } static eventListener(tag, eventType, func) { tag.addEventListener(eventType, func) } static delElement(name, loop = false, el = document) { let t = setInterval(() => { const element = el.querySelector(name) if (!element) { if (!loop) { clearInterval(t) } return } element.remove() clearInterval(t) }, 1000) } static setElement(name, style, el = document) { const element = el.querySelector(name) if (element) { for (let atr in style) { element.style[atr] = style[atr] } } } } class OperationPanel { constructor(jobListHandler) { // button this.batchPushBtn = null this.activeSwitchBtn = null this.sendSelfGreetSwitchBtn = null this.headhunterSwitchBtn = null this.bossOnlineSwitchBtn = null // inputLab // 公司名包含输入框lab this.cnInInputLab = null // 公司名排除输入框lab this.cnExInputLab = null // job名称包含输入框lab this.jnInInputLab = null // job内容排除输入框lab this.jcExInputLab = null // 薪资范围输入框lab this.srInInputLab = null // 公司规模范围输入框lab this.csrInInputLab = null // 自定义招呼语lab this.selfGreetInputLab = null // 词云图 this.worldCloudModal = null this.worldCloudState = false // false:标签 true:内容 this.worldCloudAllBtn = null this.topTitle = null // boss活跃度检测 this.bossActiveState = true; //hr在线检测 this.bossOnlineState = true; // 发送自定义招呼语 this.sendSelfGreet = false; // 猎头岗位检测 this.headhunterState = true; // 文档说明 this.docTextArr = [ "!加油,相信自己😶‍🌫️", "1.批量投递:点击批量投递开始批量投简历,请先通过上方Boss的筛选功能筛选大致的范围,然后通过脚本的筛选进一步确认投递目标。", "2.生成Job词云图:获取当前页面的所有job详情,并进行分词权重分析;生成岗位热点词汇词云图;帮助分析简历匹配度", "3.保存配置:保持下方脚本筛选项,用于后续直接使用当前配置。", "4.过滤不活跃Boss:打开后会自动过滤掉最近未活跃的Boss发布的工作。以免浪费每天的100次机会。", "5.发送自定义招呼语:因为boss不支持将自定义的招呼语设置为默认招呼语。开启表示发送boss默认的招呼语后还会发送自定义招呼语", "6.过滤猎头岗位:打开后会自动过滤掉猎头发布的工作。猎头的岗位要求一般都非常高,实际投此类岗位是无意义的,以免浪费每天的100次机会。", "7.可以在网站管理中打开通知权限,当停止时会自动发送桌面端通知提醒。", "😏", "脚本筛选项介绍:", "公司名包含:投递工作的公司名一定包含在当前集合中,模糊匹配,多个使用逗号分割。这个一般不用,如果使用了也就代表只投这些公司的岗位。例子:【阿里,华为】", "排除公司名:投递工作的公司名一定不在当前集合中,也就是排除当前集合中的公司,模糊匹配,多个使用逗号分割。例子:【xxx外包】", "排除工作内容:会自动检测上文(不是,不,无需等关键字),下文(系统,工具),例子:【外包,上门,销售,驾照】,如果写着是'不是外包''销售系统'那也不会被排除", "Job名包含:投递工作的名称一定包含在当前集合中,模糊匹配,多个使用逗号分割。例如:【软件,Java,后端,服务端,开发,后台】", "薪资范围:投递工作的薪资范围一定在当前区间中,一定是区间,使用-连接范围。例如:【12-20】", "公司规模范围:投递工作的公司人员范围一定在当前区间中,一定是区间,使用-连接范围。例如:【500-20000000】", "自定义招呼语:编辑自定义招呼语,当【发送自定义招呼语】打开时,投递后发送boss默认的招呼语【建议关闭默认打招呼语,使用自定义招呼语】后还会发送自定义招呼语;使用<br> \\n 换行;例子:【你好\\n我...】", "

作者失业,找到工作别忘了赞助支持一下哦,多谢

", "" ]; // 相关链接 this.aboutLink = [ [ ["GreasyFork", "https://greasyfork.org/zh-CN/scripts?q=%E5%BF%92%E6%98%9Fboss%E7%9B%B4%E8%81%98%E6%89%B9%E9%87%8F%E7%AE%80%E5%8E%86%E6%8A%95%E9%80%92",], ["魔改作者:忒星", "https://github.com/yongjiu8"], ["原版作者:yangfeng20", "https://github.com/yangfeng20"], ["去GitHub点个star⭐", "https://github.com/yongjiu8/boss_push"], ] ] this.scriptConfig = new ScriptConfig() this.jobListHandler = jobListHandler; } init() { this.renderOperationPanel(); this.registerEvent(); } /** * 渲染操作面板 */ renderOperationPanel() { logger.info("操作面板开始初始化") // 1.创建操作按钮并添加到按钮容器中【以下绑定事件处理函数均采用箭头函数作为中转,避免this执行事件对象】 let btnCssText = "display: inline-block;border-radius: 4px;background: #e5f8f8;color: #00a6a7; text-decoration: none;margin: 20px 20px 0px 20px;padding: 6px 12px;cursor: pointer"; // 批量投递按钮 let batchPushBtn = DOMApi.createTag("div", "批量投递", btnCssText); this.batchPushBtn = batchPushBtn DOMApi.eventListener(batchPushBtn, "click", () => { this.batchPushBtnHandler() }) // 保存配置按钮 let storeConfigBtn = DOMApi.createTag("div", "保存配置", btnCssText); DOMApi.eventListener(storeConfigBtn, "click", () => { this.storeConfigBtnHandler() }) // 生成Job词云图按钮 let generateImgBtn = DOMApi.createTag("div", "生成词云图", btnCssText); DOMApi.eventListener(generateImgBtn, "click", () => { this.worldCloudModal.style.display = "flex" this.refreshQuantity() }) // 投递后发送自定义打招呼语句 this.sendSelfGreetSwitchBtn = DOMApi.createTag("div", "发送自定义打招呼语句", btnCssText); DOMApi.eventListener(this.sendSelfGreetSwitchBtn, "click", () => { this.sendSelfGreetSwitchBtnHandler(!this.sendSelfGreet) }) this.sendSelfGreetSwitchBtnHandler(TampermonkeyApi.GmGetValue(ScriptConfig.SEND_SELF_GREET_ENABLE, false)) // 过滤不活跃boss按钮 this.activeSwitchBtn = DOMApi.createTag("div", "活跃度过滤", btnCssText); DOMApi.eventListener(this.activeSwitchBtn, "click", () => { this.activeSwitchBtnHandler(!this.bossActiveState) }) // 默认开启活跃校验 this.activeSwitchBtnHandler(this.bossActiveState) // 过滤HR在线按钮 this.bossOnlineSwitchBtn = DOMApi.createTag("div", "HR在线过滤", btnCssText); DOMApi.eventListener(this.bossOnlineSwitchBtn, "click", () => { this.bossOnlineSwitchBtnHandler(!this.bossOnlineState) }) //默认开启HR在线 this.bossOnlineSwitchBtnHandler(this.bossOnlineState) // 过滤猎头岗位 this.headhunterSwitchBtn = DOMApi.createTag("div", "过滤猎头岗位", btnCssText); DOMApi.eventListener(this.headhunterSwitchBtn, "click", () => { this.sendHeadhunterSwitchBtnHandler(!this.headhunterState) }) this.sendHeadhunterSwitchBtnHandler(TampermonkeyApi.GmGetValue(ScriptConfig.SEND_HEADHUNTER_ENABLE, true)); // 2.创建筛选条件输入框并添加到input容器中 this.cnInInputLab = DOMApi.createInputTag("公司名包含", this.scriptConfig.getCompanyNameInclude()); this.cnExInputLab = DOMApi.createInputTag("公司名排除", this.scriptConfig.getCompanyNameExclude()); this.jnInInputLab = DOMApi.createInputTag("工作名包含", this.scriptConfig.getJobNameInclude()); this.jcExInputLab = DOMApi.createInputTag("工作内容排除", this.scriptConfig.getJobContentExclude()); this.srInInputLab = DOMApi.createInputTag("薪资范围", this.scriptConfig.getSalaryRange()); this.csrInInputLab = DOMApi.createInputTag("公司规模范围", this.scriptConfig.getCompanyScaleRange()); this.selfGreetInputLab = DOMApi.createInputTag("自定义招呼语", this.scriptConfig.getSelfGreet()); DOMApi.eventListener(this.selfGreetInputLab.querySelector("input"), "blur", () => { // 失去焦点,编辑的招呼语保存到内存中;用于msgPage每次实时获取到最新的,即便不保存 ScriptConfig.setSelfGreetMemory(DOMApi.getInputVal(this.selfGreetInputLab)) }) // 每次刷新页面;将保存的数据覆盖内存临时数据;否则编辑了自定义招呼语,未保存刷新页面;发的的是之前内存中编辑的临时数据 ScriptConfig.setSelfGreetMemory(this.scriptConfig.getSelfGreet()) let inputContainerDiv = DOMApi.createTag("div", "", "margin: 10px 0px;"); inputContainerDiv.appendChild(this.cnInInputLab) inputContainerDiv.appendChild(this.cnExInputLab) inputContainerDiv.appendChild(this.jnInInputLab) inputContainerDiv.appendChild(this.jcExInputLab) inputContainerDiv.appendChild(this.srInInputLab) inputContainerDiv.appendChild(this.csrInInputLab) inputContainerDiv.appendChild(this.selfGreetInputLab) // 进度显示 this.showTable = this.buildShowTable(); // 操作面板结构: let operationPanel = DOMApi.createTag("div"); // 说明文档 // 链接关于 // 操作按钮 // 筛选输入框 // iframe【详情页投递内部页】 operationPanel.appendChild(this.buildDocDiv()) operationPanel.appendChild(inputContainerDiv) // 发送自定义招呼语的iframe operationPanel.appendChild(this.buildMsgPageIframe()) operationPanel.appendChild(this.showTable) // 词云图模态框 加到根节点 document.body.appendChild(this.buildWordCloudModel()) // 找到页面锚点并将操作面板添加入页面 let timingCutPageTask = setInterval(() => { logger.info("等待页面加载,添加操作面板") // 页面锚点 const jobSearchWrapper = document.querySelector(".job-search-wrapper") if (!jobSearchWrapper) { return; } const jobConditionWrapper = jobSearchWrapper.querySelector(".search-condition-wrapper") if (!jobConditionWrapper) { return } let topTitle = DOMApi.createTag("h2"); this.topTitle = topTitle; topTitle.textContent = `Boos直聘投递助手(${this.scriptConfig.getVal(ScriptConfig.PUSH_COUNT, 0)}次) 记得 star⭐`; jobConditionWrapper.insertBefore(topTitle, jobConditionWrapper.firstElementChild) // 按钮/搜索换位 const jobSearchBox = jobSearchWrapper.querySelector(".job-search-box") jobSearchBox.style.margin = "20px 0" jobSearchBox.style.width = "100%" const city = jobConditionWrapper.querySelector(".city-area-select") city.querySelector(".city-area-current").style.width = "85px" const condition = jobSearchWrapper.querySelectorAll(".condition-industry-select,.condition-position-select,.condition-filter-select,.clear-search-btn") const cityAreaDropdown = jobSearchWrapper.querySelector(".city-area-dropdown") cityAreaDropdown.insertBefore(jobSearchBox, cityAreaDropdown.firstElementChild) const filter = DOMApi.createTag("div", "", "overflow:hidden ") condition.forEach(item => { filter.appendChild(item) }) filter.appendChild(DOMApi.createTag("div", "", "clear:both")) cityAreaDropdown.appendChild(filter) const bttt = [batchPushBtn, generateImgBtn, storeConfigBtn, this.activeSwitchBtn, this.bossOnlineSwitchBtn, this.sendSelfGreetSwitchBtn, this.headhunterSwitchBtn] bttt.forEach(item => { jobConditionWrapper.appendChild(item); }) cityAreaDropdown.appendChild(operationPanel); clearInterval(timingCutPageTask); logger.info("初始化【操作面板】成功") // 页面美化 this.pageBeautification() }, 1000); } /** * 页面美化 */ pageBeautification() { // 侧栏 DOMApi.delElement(".job-side-wrapper") // 侧边悬浮框 DOMApi.delElement(".side-bar-box") // 新职位发布时通知我 DOMApi.delElement(".subscribe-weixin-wrapper", true) // 搜索栏登录框 DOMApi.delElement(".go-login-btn") // 搜索栏去APP DOMApi.delElement(".job-search-scan", true) // 顶部面板 // DOMApi.setElement(".job-search-wrapper",{width:"90%"}) // DOMApi.setElement(".page-job-content",{width:"90%"}) // DOMApi.setElement(".job-list-wrapper",{width:"100%"}) GM_addStyle(` .job-search-wrapper,.page-job-content{width: 90% !important} .job-list-wrapper,.job-card-wrapper,.job-search-wrapper.fix-top{width: 100% !important} .job-card-wrapper .job-card-body{display: flex;justify-content: space-between;} .job-card-wrapper .job-card-left{width: 50% !important} .job-card-wrapper .start-chat-btn,.job-card-wrapper:hover .info-public{display: initial !important} .job-card-wrapper .job-card-footer{min-height: 48px;display: flex;justify-content: space-between} .job-card-wrapper .clearfix:after{content: none} .job-card-wrapper .job-card-footer .info-desc{width: auto !important} .job-card-wrapper .job-card-footer .tag-list{width: auto !important;margin-right:10px} .city-area-select.pick-up .city-area-dropdown{width: 80vw;min-width: 1030px;} .job-search-box .job-search-form{width: 100%;} .job-search-box .job-search-form .city-label{width: 10%;} .job-search-box .job-search-form .search-input-box{width: 82%;} .job-search-box .job-search-form .search-btn{width: 8%;} .job-search-wrapper.fix-top .job-search-box, .job-search-wrapper.fix-top .search-condition-wrapper{width: 90%;min-width:990px;} `) logger.info("初始化【页面美化】成功") } registerEvent() { TampermonkeyApi.GmAddValueChangeListener(ScriptConfig.PUSH_COUNT, this.publishCountChangeEventHandler.bind(this)) } refreshShow(text) { this.showTable.innerHTML = "当前操作:" + text } refreshQuantity() { this.worldCloudAllBtn.innerHTML = `生成全部(${this.jobListHandler.cacheSize()}个)` } /*-------------------------------------------------构建复合DOM元素--------------------------------------------------*/ buildDocDiv() { const docDiv = DOMApi.createTag("div", "", "margin: 10px 0px; width: 100%;") let txtDiv = DOMApi.createTag("div", "", "display: block;"); const title = DOMApi.createTag("h3", "操作说明(点击关闭)", "margin: 10px 0px;cursor: pointer") docDiv.appendChild(title) docDiv.appendChild(txtDiv) this.docTextArr.forEach(doc => { const textTag = document.createElement("p"); textTag.style.color = "#666"; textTag.innerHTML = doc; txtDiv.appendChild(textTag) }) this.aboutLink.forEach((linkMap) => { let about = DOMApi.createTag("p", "", "padding-top: 12px;"); linkMap.forEach((item) => { const a = document.createElement("a"); a.innerText = item[0]; a.href = item[1]; a.target = "_blank"; a.style.margin = "0 20px 0 0"; about.appendChild(a); }); txtDiv.appendChild(about); }); // 点击title,内部元素折叠 DOMApi.eventListener(title, "click", () => { let divDisplay = txtDiv.style.display; if (divDisplay === 'block' || divDisplay === '') { txtDiv.style.display = 'none'; } else { txtDiv.style.display = 'block'; } }) return docDiv; } buildMsgPageIframe() { let msgPageIframe = DOMApi.createTag("iframe", "", "height:1px;width: 1px;"); msgPageIframe.src = 'https://www.zhipin.com/web/geek/chat'; msgPageIframe.id = 'msgIframe'; return msgPageIframe } buildShowTable() { return DOMApi.createTag('p', '', 'font-size: 20px;color: rgb(64, 158, 255);margin-left: 50px;'); } buildWordCloudModel() { this.worldCloudModal = DOMApi.createTag("div", `

词云图

`, "display: none;") const model = this.worldCloudModal model.className = "dialog-wrap" model.querySelector(".close").onclick = function () { model.style.display = "none"; } const body = model.querySelector(".dialog-body") const div = DOMApi.createTag("div") let btnCssText = "display: inline-block;border-radius: 4px;background: #e5f8f8;color: #00a6a7; text-decoration: none;margin: 0px 20px;padding: 6px 12px;cursor: pointer"; // 当前状态 let stateBtn = DOMApi.createTag("div", "状态: 工作标签", btnCssText); DOMApi.eventListener(stateBtn, "click", () => { if (this.worldCloudState) { stateBtn.innerHTML = "状态: 工作标签" } else { stateBtn.innerHTML = "状态: 工作内容" } this.worldCloudState = !this.worldCloudState }) // 爬取当前页面生成词云 let curBtn = DOMApi.createTag("div", "生成当前页", btnCssText); DOMApi.eventListener(curBtn, "click", () => { if (this.worldCloudState) { this.generateImgHandler() } else { this.generateImgHandlerJobLabel() } }) // 根据已爬取的数据生成词云 let allBtn = DOMApi.createTag("div", "生成全部(0个)", btnCssText); DOMApi.eventListener(allBtn, "click", () => { if (this.worldCloudState) { // this.generateImgHandlerAll() window.alert("卡顿严重,数据量大已禁用,请用标签模式") } else { this.generateImgHandlerJobLabelAll() } }) this.worldCloudAllBtn = allBtn // 清空已爬取的数据 let delBtn = DOMApi.createTag("div", "清空数据", btnCssText); DOMApi.eventListener(delBtn, "click", () => { this.jobListHandler.cacheClear() this.refreshQuantity() }) div.appendChild(stateBtn) div.appendChild(curBtn) div.appendChild(allBtn) div.appendChild(delBtn) body.insertBefore(div, body.firstElementChild) return this.worldCloudModal } /*-------------------------------------------------操作面板事件处理--------------------------------------------------*/ batchPushBtnHandler() { this.jobListHandler.batchPushHandler() } /** * 生成词云图 * 使用的数据源为 job工作内容,进行分词 */ generateImgHandler() { let jobList = BossDOMApi.getJobList(); let allJobContent = "" this.refreshShow("生成词云图【获取Job数据中】") Array.from(jobList).reduce((promiseChain, jobTag) => { return promiseChain .then(() => this.jobListHandler.reqJobDetail(jobTag)) .then(jobCardJson => { allJobContent += jobCardJson.postDescription + "" }) }, Promise.resolve()) .then(() => { this.refreshShow("生成词云图【构建数据中】") return JobWordCloud.participle(allJobContent) }).then(worldArr => { let weightWordArr = JobWordCloud.buildWord(worldArr); logger.info("根据权重排序的world结果:", JobWordCloud.getKeyWorldArr(weightWordArr)); JobWordCloud.generateWorldCloudImage("worldCloudCanvas", weightWordArr) this.refreshShow("生成词云图【完成】") }) } /** * 生成词云图 * 使用的数据源为 job标签,并且不进行分词,直接计算权重 */ generateImgHandlerJobLabel() { let jobList = BossDOMApi.getJobList(); let jobLabelArr = [] this.refreshShow("生成词云图【获取Job数据中】") Array.from(jobList).reduce((promiseChain, jobTag) => { return promiseChain .then(() => this.jobListHandler.reqJobDetail(jobTag)) .then(jobCardJson => { jobLabelArr.push(...jobCardJson.jobLabels) }) }, Promise.resolve()) .then(() => { this.refreshShow("生成词云图【构建数据中】") let weightWordArr = JobWordCloud.buildWord(jobLabelArr); logger.info("根据权重排序的world结果:", JobWordCloud.getKeyWorldArr(weightWordArr)); this.worldCloudModal.style.display = "flex" JobWordCloud.generateWorldCloudImage("worldCloudCanvas", weightWordArr) this.refreshShow("生成词云图【完成】") }) } /** * 生成All词云图 * 使用的数据源为 job工作内容,进行分词 */ generateImgHandlerAll() { let allJobContent = "" this.jobListHandler.cache.forEach((val) => { allJobContent += val.postDescription }) Promise.resolve() .then(() => { this.refreshShow("生成词云图【构建数据中】") return JobWordCloud.participle(allJobContent) }).then(worldArr => { let weightWordArr = JobWordCloud.buildWord(worldArr); logger.info("根据权重排序的world结果:", JobWordCloud.getKeyWorldArr(weightWordArr)); JobWordCloud.generateWorldCloudImage("worldCloudCanvas", weightWordArr) this.refreshShow("生成词云图【完成】") }) } /** * 生成All词云图 * 使用的数据源为 job标签,并且不进行分词,直接计算权重 */ generateImgHandlerJobLabelAll() { let jobLabelArr = [] this.jobListHandler.cache.forEach((val) => { jobLabelArr.push(...val.jobLabels) }) this.refreshShow("生成词云图【获取Job数据中】") Promise.resolve() .then(() => { this.refreshShow("生成词云图【构建数据中】") let weightWordArr = JobWordCloud.buildWord(jobLabelArr); logger.info("根据权重排序的world结果:", JobWordCloud.getKeyWorldArr(weightWordArr)); this.worldCloudModal.style.display = "flex" JobWordCloud.generateWorldCloudImage("worldCloudCanvas", weightWordArr) this.refreshShow("生成词云图【完成】") }) } readInputConfig() { this.scriptConfig.setCompanyNameInclude(DOMApi.getInputVal(this.cnInInputLab)) this.scriptConfig.setCompanyNameExclude(DOMApi.getInputVal(this.cnExInputLab)) this.scriptConfig.setJobNameInclude(DOMApi.getInputVal(this.jnInInputLab)) this.scriptConfig.setJobContentExclude(DOMApi.getInputVal(this.jcExInputLab)) this.scriptConfig.setSalaryRange(DOMApi.getInputVal(this.srInInputLab)) this.scriptConfig.setCompanyScaleRange(DOMApi.getInputVal(this.csrInInputLab)) this.scriptConfig.setSelfGreet(DOMApi.getInputVal(this.selfGreetInputLab)) } storeConfigBtnHandler() { // 先修改配置对象内存中的值,然后更新到本地储存中 this.readInputConfig() logger.info("config", this.scriptConfig) this.scriptConfig.storeConfig() } activeSwitchBtnHandler(isOpen) { this.bossActiveState = isOpen; if (this.bossActiveState) { this.activeSwitchBtn.innerText = "过滤不活跃Boss:已开启"; this.activeSwitchBtn.style.backgroundColor = "rgb(215,254,195)"; this.activeSwitchBtn.style.color = "rgb(2,180,6)"; } else { this.activeSwitchBtn.innerText = "过滤不活跃Boss:已关闭"; this.activeSwitchBtn.style.backgroundColor = "rgb(251,224,224)"; this.activeSwitchBtn.style.color = "rgb(254,61,61)"; } this.scriptConfig.setVal(ScriptConfig.ACTIVE_ENABLE, isOpen) } //检查是否在线 bossOnlineSwitchBtnHandler(isOpen) { this.bossOnlineState = isOpen; if (this.bossOnlineState) { this.bossOnlineSwitchBtn.innerText = "过滤HR在线:已开启"; this.bossOnlineSwitchBtn.style.backgroundColor = "rgb(215,254,195)"; this.bossOnlineSwitchBtn.style.color = "rgb(2,180,6)"; } else { this.bossOnlineSwitchBtn.innerText = "过滤HR在线:已关闭"; this.bossOnlineSwitchBtn.style.backgroundColor = "rgb(251,224,224)"; this.bossOnlineSwitchBtn.style.color = "rgb(254,61,61)"; } this.scriptConfig.setVal(ScriptConfig.BOSS_ONLINE_ENABLE, isOpen) } sendSelfGreetSwitchBtnHandler(isOpen) { this.sendSelfGreet = isOpen; if (isOpen) { this.sendSelfGreetSwitchBtn.innerText = "发送自定义招呼语:已开启"; this.sendSelfGreetSwitchBtn.style.backgroundColor = "rgb(215,254,195)"; this.sendSelfGreetSwitchBtn.style.color = "rgb(2,180,6)"; } else { this.sendSelfGreetSwitchBtn.innerText = "发送自定义招呼语:已关闭"; this.sendSelfGreetSwitchBtn.style.backgroundColor = "rgb(251,224,224)"; this.sendSelfGreetSwitchBtn.style.color = "rgb(254,61,61)"; } this.scriptConfig.setVal(ScriptConfig.SEND_SELF_GREET_ENABLE, isOpen) } sendHeadhunterSwitchBtnHandler(isOpen) { this.headhunterState = isOpen; if (isOpen) { this.headhunterSwitchBtn.innerText = "过滤猎头岗位:已开启"; this.headhunterSwitchBtn.style.backgroundColor = "rgb(215,254,195)"; this.headhunterSwitchBtn.style.color = "rgb(2,180,6)"; } else { this.headhunterSwitchBtn.innerText = "过滤猎头岗位:已关闭"; this.headhunterSwitchBtn.style.backgroundColor = "rgb(251,224,224)"; this.headhunterSwitchBtn.style.color = "rgb(254,61,61)"; } this.scriptConfig.setVal(ScriptConfig.SEND_HEADHUNTER_ENABLE, isOpen) } publishCountChangeEventHandler(key, oldValue, newValue, isOtherScriptChange) { this.topTitle.textContent = `Boos直聘投递助手(${newValue}次) 记得 star⭐`; logger.info("投递次数变更事件", {key, oldValue, newValue, isOtherScriptChange}) } /*-------------------------------------------------other method--------------------------------------------------*/ changeBatchPublishBtn(start) { if (start) { this.batchPushBtn.innerHTML = "停止投递" this.batchPushBtn.style.backgroundColor = "rgb(251,224,224)"; this.batchPushBtn.style.color = "rgb(254,61,61)"; } else { this.batchPushBtn.innerHTML = "批量投递" this.batchPushBtn.style.backgroundColor = "rgb(215,254,195)"; this.batchPushBtn.style.color = "rgb(2,180,6)"; } } } class ScriptConfig extends TampermonkeyApi { static LOCAL_CONFIG = "config"; static PUSH_COUNT = "pushCount:" + ScriptConfig.getCurDay(); static ACTIVE_ENABLE = "activeEnable"; static PUSH_LIMIT = "push_limit" + ScriptConfig.getCurDay(); // 投递锁是否被占用,可重入;value表示当前正在投递的job static PUSH_LOCK = "push_lock"; static PUSH_MESSAGE = "push_message"; static SEND_SELF_GREET_ENABLE = "sendSelfGreetEnable"; static SEND_HEADHUNTER_ENABLE = "sendHeadhunterEnable"; //HR在线检测 static BOSS_ONLINE_ENABLE = "bossOnlineEnable"; // 公司名包含输入框lab static cnInKey = "companyNameInclude" // 公司名排除输入框lab static cnExKey = "companyNameExclude" // job名称包含输入框lab static jnInKey = "jobNameInclude" // job内容排除输入框lab static jcExKey = "jobContentExclude" // 薪资范围输入框lab static srInKey = "salaryRange" // 公司规模范围输入框lab static csrInKey = "companyScaleRange" // 自定义招呼语输入框 static sgInKey = "sendSelfGreet" static SEND_SELF_GREET_MEMORY = "sendSelfGreetMemory" constructor() { super(); this.configObj = {} this.loaderConfig() } static getCurDay() { // 创建 Date 对象获取当前时间 const currentDate = new Date(); // 获取年、月、日、小时、分钟和秒 const year = currentDate.getFullYear(); const month = String(currentDate.getMonth() + 1).padStart(2, '0'); const day = String(currentDate.getDate()).padStart(2, '0'); // 格式化时间字符串 return `${year}-${month}-${day}`; } static pushCountIncr() { let number = TampermonkeyApi.GmGetValue(ScriptConfig.PUSH_COUNT, 0); TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_COUNT, ++number) } getVal(key, defVal) { return TampermonkeyApi.GmGetValue(key, defVal) } setVal(key, val) { TampermonkeyApi.GmSetValue(key, val) } getArrConfig(key, isArr) { let arr = this.configObj[key]; if (isArr) { return arr; } if (!arr) { return ""; } return arr.join(","); } getStrConfig(key) { let str = this.configObj[key]; if (!str) { return ""; } return str; } getCompanyNameInclude(isArr) { return this.getArrConfig(ScriptConfig.cnInKey, isArr); } getCompanyNameExclude(isArr) { return this.getArrConfig(ScriptConfig.cnExKey, isArr); } getJobContentExclude(isArr) { return this.getArrConfig(ScriptConfig.jcExKey, isArr); } getJobNameInclude(isArr) { return this.getArrConfig(ScriptConfig.jnInKey, isArr); } getSalaryRange() { return this.getStrConfig(ScriptConfig.srInKey); } getCompanyScaleRange() { return this.getStrConfig(ScriptConfig.csrInKey); } getSelfGreet() { return this.getStrConfig(ScriptConfig.sgInKey); } setCompanyNameInclude(val) { return this.configObj[ScriptConfig.cnInKey] = val.split(","); } setCompanyNameExclude(val) { this.configObj[ScriptConfig.cnExKey] = val.split(","); } setJobNameInclude(val) { this.configObj[ScriptConfig.jnInKey] = val.split(","); } setJobContentExclude(val) { this.configObj[ScriptConfig.jcExKey] = val.split(","); } setSalaryRange(val) { this.configObj[ScriptConfig.srInKey] = val; } setCompanyScaleRange(val) { this.configObj[ScriptConfig.csrInKey] = val; } setSelfGreet(val) { this.configObj[ScriptConfig.sgInKey] = val; } static setSelfGreetMemory(val) { TampermonkeyApi.GmSetValue(ScriptConfig.SEND_SELF_GREET_MEMORY, val) } getSelfGreetMemory() { let value = TampermonkeyApi.GmGetValue(ScriptConfig.SEND_SELF_GREET_MEMORY); if (value) { return value; } return this.getSelfGreet(); } /** * 存储配置到本地存储中 */ storeConfig() { let configStr = JSON.stringify(this.configObj); TampermonkeyApi.GmSetValue(ScriptConfig.LOCAL_CONFIG, configStr); logger.info("存储配置到本地储存", configStr) } /** * 从本地存储中加载配置 */ loaderConfig() { let localConfig = TampermonkeyApi.GmGetValue(ScriptConfig.LOCAL_CONFIG, ""); if (!localConfig) { logger.warn("未加载到本地配置") return; } this.configObj = JSON.parse(localConfig); logger.info("成功加载本地配置", this.configObj) } } class BossDOMApi { static getJobList() { return document.querySelectorAll(".job-card-wrapper"); } static getJobTitle(jobTag) { let innerText = jobTag.querySelector(".job-title").innerText; return innerText.replace("\n", " "); } //是猎头发布的职位吗? static isHeadhunter(jobTag) { let jobTagIcon = jobTag.querySelector("img.job-tag-icon"); return !!jobTagIcon; } static getCompanyName(jobTag) { return jobTag.querySelector(".company-name").innerText; } static getJobName(jobTag) { return jobTag.querySelector(".job-name").innerText; } static getSalaryRange(jobTag) { let text = jobTag.querySelector(".salary").innerText; if (text.includes(".")) { // 1-2K·13薪 return text.split("·")[0]; } return text; } static getCompanyScaleRange(jobTag) { return jobTag.querySelector(".company-tag-list").lastElementChild.innerHTML; } /** * 获取当前job标签的招聘人名称以及他的职位 * @param jobTag */ static getBossNameAndPosition(jobTag) { let nameAndPositionTextArr = jobTag.querySelector(".info-public").innerHTML.split(""); nameAndPositionTextArr[0] = nameAndPositionTextArr[0].trim(); nameAndPositionTextArr[1] = nameAndPositionTextArr[1].replace("", "").trim(); return nameAndPositionTextArr; } /** * 是否为未沟通 * @param jobTag */ static isNotCommunication(jobTag) { const jobStatusStr = jobTag.querySelector(".start-chat-btn").innerText; return jobStatusStr.includes("立即沟通"); } static getJobDetailUrlParams(jobTag) { return jobTag.querySelector(".job-card-left").href.split("?")[1] } static getDetailSrc(jobTag) { return jobTag.querySelector(".job-card-left").href; } static getUniqueKey(jobTag) { const title = this.getJobTitle(jobTag) const company = this.getCompanyName(jobTag) return `${title}--${company}` } static nextPage() { let nextPageBtn = document.querySelector(".ui-icon-arrow-right"); if (nextPageBtn.parentElement.className === "disabled") { // 没有下一页 return; } nextPageBtn.click(); return true; } } class JobListPageHandler { constructor() { this.operationPanel = new OperationPanel(this); this.scriptConfig = this.operationPanel.scriptConfig this.operationPanel.init() this.publishState = false this.nextPage = false this.mock = false this.cache = new Map() this.selfDefCount = -1 } /** * 点击批量投递事件处理 */ batchPushHandler() { this.changeBatchPublishState(!this.publishState); if (!this.publishState) { return; } // 每次投递前清空投递锁,未被占用 this.scriptConfig.setVal(ScriptConfig.PUSH_LIMIT, false) TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_LOCK, "") // 每次读取操作面板中用户实时输入的值 this.operationPanel.readInputConfig() this.loopPublish() } loopPublish() { // 过滤当前页满足条件的job并投递 this.filterCurPageAndPush() // 等待处理完当前页的jobList在投递下一页 let nextPageTask = setInterval(() => { if (!this.nextPage) { logger.info("正在等待当前页投递完毕...") return; } clearInterval(nextPageTask) if (!this.publishState) { logger.info("投递结束") TampermonkeyApi.GmNotification("投递结束") this.operationPanel.refreshShow("投递停止") this.changeBatchPublishState(false); return; } if (!BossDOMApi.nextPage()) { logger.info("投递结束,没有下一页") TampermonkeyApi.GmNotification("投递结束,没有下一页") this.operationPanel.refreshShow("投递结束,没有下一页") this.changeBatchPublishState(false); return; } this.operationPanel.refreshShow("开始等待 10 秒钟,进行下一页") // 点击下一页,需要等待页面元素变化,否则将重复拿到当前页的jobList setTimeout(() => { this.loopPublish() }, 10000) }, 3000); } changeBatchPublishState(publishState) { this.publishState = publishState; this.operationPanel.changeBatchPublishBtn(publishState) } filterCurPageAndPush() { this.nextPage = false; let notMatchCount = 0; let publishResultCount = { successCount: 0, failCount: 0, } let jobList = BossDOMApi.getJobList(); logger.info("jobList", jobList) let process = Array.from(jobList).reduce((promiseChain, jobTag) => { let jobTitle = BossDOMApi.getJobTitle(jobTag); return promiseChain .then(() => this.matchJobPromise(jobTag)) .then(() => this.reqJobDetail(jobTag)) .then(jobCardJson => this.jobDetailFilter(jobTag, jobCardJson)) .then(() => this.sendPublishReq(jobTag)) .then(publishResult => this.handlerPublishResult(jobTag, publishResult, publishResultCount)) .catch(error => { // 在catch中return是结束当前元素,不会结束整个promiseChain; // 需要结束整个promiseChain,在catch throw exp,但还会继续执行下一个元素catch中的逻辑 switch (true) { case error instanceof JobNotMatchExp: this.operationPanel.refreshShow(jobTitle + " 不满足投递条件") ++notMatchCount; break; case error instanceof FetchJobDetailFailExp: logger.error("job详情页数据获取失败:" + error); break; case error instanceof SendPublishExp: logger.error("投递失败;" + jobTitle + " 原因:" + error.message); this.operationPanel.refreshShow(jobTitle + " 投递失败") publishResultCount.failCount++ break; case error instanceof PublishLimitExp: TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_LIMIT, true); this.operationPanel.refreshShow("停止投递 " + error.message) logger.error("投递停止; 原因:" + error.message); throw new PublishStopExp(error.message) case error instanceof PublishStopExp: this.changeBatchPublishState(false) // 结束整个投递链路 throw error; default: logger.info(BossDOMApi.getDetailSrc(jobTag) + "-->未捕获投递异常:", error); } }) }, Promise.resolve()).catch(error => { // 这里只是让报错不显示,不需要处理异常 }); // 当前页jobList中所有job处理完毕执行 process.finally(() => { logger.info("当前页投递完毕---------------------------------------------------") logger.info("不满足条件的job数量:" + notMatchCount) logger.info("投递Job成功数量:" + publishResultCount.successCount) logger.info("投递Job失败数量:" + publishResultCount.failCount) logger.info("当前页投递完毕---------------------------------------------------") this.nextPage = true; }) } cacheClear() { this.cache.clear() } cacheSize() { return this.cache.size } reqJobDetail(jobTag, retries = 3) { return new Promise((resolve, reject) => { if (retries === 0) { return reject(new FetchJobDetailFailExp()); } // todo 如果在投递当前页中,点击停止投递,那么当前页重新投递的话,会将已经投递的再重新投递一遍 // 原因是没有重新获取数据;沟通状态还是立即沟通,实际已经投递过一遍,已经为继续沟通 // 暂时不影响逻辑,重复投递,boss自己会过滤,不会重复发送消息;发送自定义招呼语也没问题;油猴会过滤【oldVal===newVal】的数据,也就不会重复发送自定义招呼语 const key = BossDOMApi.getUniqueKey(jobTag) if (this.cache.has(key)) { return resolve(this.cache.get(key)) } let params = BossDOMApi.getJobDetailUrlParams(jobTag); axios.get("https://www.zhipin.com/wapi/zpgeek/job/card.json?" + params, {timeout: 5000}) .then(resp => { this.cache.set(key, resp.data.zpData.jobCard) return resolve(resp.data.zpData.jobCard); }).catch(error => { logger.info("获取详情页异常正在重试:", error) return this.reqJobDetail(jobTag, retries - 1) }) }) } jobDetailFilter(jobTag, jobCardJson) { let jobTitle = BossDOMApi.getJobTitle(jobTag); return new Promise((resolve, reject) => { //检查是否在线 let bossOnlineCheck = TampermonkeyApi.GmGetValue(ScriptConfig.BOSS_ONLINE_ENABLE, true); logger.info("当前职位【" + jobTitle + "】HR在线状态:" + jobCardJson.online) if (bossOnlineCheck && !jobCardJson.online) { logger.info("当前job被过滤:【" + jobTitle + "】 HR原因:不在线") return reject(new JobNotMatchExp()) } // 工作详情活跃度检查 let activeCheck = TampermonkeyApi.GmGetValue(ScriptConfig.ACTIVE_ENABLE, true); let activeTimeDesc = jobCardJson.activeTimeDesc; if (activeCheck && !Tools.bossIsActive(activeTimeDesc)) { logger.info("当前boss活跃度:" + activeTimeDesc) logger.info("当前job被过滤:【" + jobTitle + "】 原因:不满足活跃度检查") return reject(new JobNotMatchExp()) } // 猎头工作岗位检查 let headhunterCheck = TampermonkeyApi.GmGetValue(ScriptConfig.SEND_HEADHUNTER_ENABLE, true); if (headhunterCheck && BossDOMApi.isHeadhunter(jobTag)) { logger.info("当前工作为猎头发布:" + jobTitle); logger.info("当前job被过滤:【" + jobTitle + "】 原因:为猎头发布的工作"); return reject(new JobNotMatchExp()); } // 工作内容检查 let jobContentExclude = this.scriptConfig.getJobContentExclude(true); const jobContentMismatch = Tools.semanticMatch(jobContentExclude, jobCardJson.postDescription) if (jobContentMismatch) { logger.info("当前job工作内容:" + jobCardJson.postDescription) logger.info(`当前job被过滤:【${jobTitle}】 原因:不满足工作内容(${jobContentMismatch})`) return reject(new JobNotMatchExp()) } setTimeout(() => { // 获取不同的延时,避免后面投递时一起导致频繁 return resolve(); }, Tools.getRandomNumber(100, 200)) }) } handlerPublishResult(jobTag, result, publishResultCount) { return new Promise((resolve, reject) => { if (result.message === 'Success' && result.code === 0) { // 增加投递数量,触发投递监听,更新页面投递计数 ScriptConfig.pushCountIncr() publishResultCount.successCount++ logger.info("投递成功:" + BossDOMApi.getJobTitle(jobTag)) // 改变消息key,通知msg页面处理当前job发送自定义招呼语句 TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_MESSAGE, JobMessagePageHandler.buildMsgKey(jobTag)) // 每页投递次数【默认不会走】 if (this.selfDefCount !== -1 && publishResultCount.successCount >= this.selfDefCount) { return reject(new PublishStopExp("自定义投递限制:" + this.selfDefCount)) } return resolve() } if (result.message.includes("今日沟通人数已达上限")) { return reject(new PublishLimitExp(result.message)) } return reject(new SendPublishExp(result.message)) }) } sendPublishReq(jobTag, errorMsg, retries = 3) { let jobTitle = BossDOMApi.getJobTitle(jobTag); if (retries === 3) { logger.info("正在投递:" + jobTitle) } return new Promise((resolve, reject) => { if (retries === 0) { return reject(new SendPublishExp(errorMsg)); } if (!this.publishState) { return reject(new PublishStopExp("停止投递")) } // 检查投递限制 let pushLimit = TampermonkeyApi.GmGetValue(ScriptConfig.PUSH_LIMIT, false); if (pushLimit) { this.changeBatchPublishState(false) return reject(new PublishLimitExp("boss投递限制每天100次")) } if (this.mock) { let result = { message: 'Success', code: 0 } return resolve(result) } let src = BossDOMApi.getDetailSrc(jobTag); let paramObj = Tools.parseURL(src); let publishUrl = "https://www.zhipin.com/wapi/zpgeek/friend/add.json" let url = Tools.queryString(publishUrl, paramObj); let pushLockTask = setInterval(() => { if (!this.publishState) { clearInterval(pushLockTask) return reject(new PublishStopExp()) } let lock = TampermonkeyApi.GmGetValue(ScriptConfig.PUSH_LOCK, ""); if (lock && lock !== jobTitle) { return logger.info("投递锁被其他job占用:" + lock) } // 停止锁检查并占用投递锁 clearInterval(pushLockTask) TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_LOCK, jobTitle) logger.info("锁定投递锁:" + jobTitle) this.operationPanel.refreshShow("正在投递-->" + jobTitle) // 投递请求 axios.post(url, null, {headers: {"Zp_token": Tools.getCookieValue("bst")}}) .then(resp => { if (resp.data.code === 1 && resp.data?.zpData?.bizData?.chatRemindDialog?.content) { // 某些条件不满足,boss限制投递,无需重试,在结果处理器中处理 return resolve({ code: 1, message: resp.data?.zpData?.bizData?.chatRemindDialog?.content }) } if (resp.data.code !== 0) { throw new SendPublishExp(resp.data.message) } return resolve(resp.data); }).catch(error => { logger.info("投递异常正在重试:" + jobTitle, error) return resolve(this.sendPublishReq(jobTag, error.message, retries - 1)) }).finally(() => { // 释放投递锁 logger.info("释放投递锁:" + jobTitle) TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_LOCK, "") }) }, 800); }) } matchJobPromise(jobTag) { return new Promise(((resolve, reject) => { if (!this.matchJob(jobTag)) { return reject(new JobNotMatchExp()) } return resolve(jobTag) })) } matchJob(jobTag) { let jobTitle = BossDOMApi.getJobTitle(jobTag); let pageCompanyName = BossDOMApi.getCompanyName(jobTag); // 不满足配置公司名 if (!Tools.fuzzyMatch(this.scriptConfig.getCompanyNameInclude(true), pageCompanyName, true)) { logger.info("当前公司名:" + pageCompanyName) logger.info("当前job被过滤:【" + jobTitle + "】 原因:不满足配置公司名") return false; } // 满足排除公司名 if (Tools.fuzzyMatch(this.scriptConfig.getCompanyNameExclude(true), pageCompanyName, false)) { logger.info("当前公司名:" + pageCompanyName) logger.info("当前job被过滤:【" + jobTitle + "】 原因:满足排除公司名") return false; } // 不满足配置工作名 let pageJobName = BossDOMApi.getJobName(jobTag); if (!Tools.fuzzyMatch(this.scriptConfig.getJobNameInclude(true), pageJobName, true)) { logger.info("当前工作名:" + pageJobName) logger.info("当前job被过滤:【" + jobTitle + "】 原因:不满足配置工作名") return false; } // 不满足新增范围 let pageSalaryRange = BossDOMApi.getSalaryRange(jobTag); let salaryRange = this.scriptConfig.getSalaryRange(); if (!Tools.rangeMatch(salaryRange, pageSalaryRange)) { logger.info("当前薪资范围:" + pageSalaryRange) logger.info("当前job被过滤:【" + jobTitle + "】 原因:不满足薪资范围") return false; } let pageCompanyScaleRange = this.scriptConfig.getCompanyScaleRange(); if (!Tools.rangeMatch(pageCompanyScaleRange, BossDOMApi.getCompanyScaleRange(jobTag))) { logger.info("当前公司规模范围:" + pageCompanyScaleRange) logger.info("当前job被过滤:【" + jobTitle + "】 原因:不满足公司规模范围") return false; } if (!BossDOMApi.isNotCommunication(jobTag)) { logger.info("当前job被过滤:【" + jobTitle + "】 原因:已经沟通过") return false; } return true; } } class JobMessagePageHandler { constructor() { this.scriptConfig = new ScriptConfig(); this.init() } init() { this.registerEvent(); } registerEvent() { TampermonkeyApi.GmAddValueChangeListener(ScriptConfig.PUSH_MESSAGE, this.pushAlterMsgHandler.bind(this)) logger.info("注册投递推送消费者成功") } /** * 投递后发送自定义打招呼语句【发送自定义消息】 */ pushAlterMsgHandler(key, oldValue, newValue, isOtherScriptChange) { logger.info("投递后推送自定义招呼语消费者", {key, oldValue, newValue, isOtherScriptChange}) if (!isOtherScriptChange) { return; } if (oldValue === newValue) { return; } // 是否打开配置 if (!TampermonkeyApi.GmGetValue(ScriptConfig.SEND_SELF_GREET_ENABLE, false)) { return; } let selfGreetMsg = this.getSelfGreet(); if (!selfGreetMsg) { logger.info("自定义招呼语为空结束") return; } let count = 0; let process = Promise.resolve() let sendMsgTask = setInterval(() => { process.then(() => { if (++count >= 5) { logger.info("发送自定义打招呼语句超时结束") clearInterval(sendMsgTask); return; } return new Promise(async (resolve, reject) => { let msgTag = await JobMessagePageHandler.selectMessage(newValue); if (!msgTag) { return reject(); } // 点击当前待处理的消息框 msgTag.click(); logger.info("选中消息", msgTag) return resolve(); }) }).then(() => { return new Promise((resolve, reject) => { if (!JobMessagePageHandler.ableInput()) { return reject(); } return resolve(); }) }).then(() => { return new Promise((resolve => { JobMessagePageHandler.inputMsg(selfGreetMsg) return resolve(); })) }).then(() => { return new Promise(((resolve, reject) => { if (!JobMessagePageHandler.sendAble()) { return reject(); } return resolve(); })) }).then(() => { return new Promise((resolve => { JobMessagePageHandler.sendMsg() logger.info("推送自定义招呼语成功:" + newValue) clearInterval(sendMsgTask) return resolve() })) }).catch(() => { // 不报错 }) }, 500); } getSelfGreet() { return this.scriptConfig.getSelfGreetMemory(); } static buildMsgKey(jobTag) { let companyName = BossDOMApi.getCompanyName(jobTag); let bossNameAndPosition = BossDOMApi.getBossNameAndPosition(jobTag); let bossName = bossNameAndPosition[0]; let bossPositionName = bossNameAndPosition[1]; return bossName + companyName + bossPositionName; } static ableInput() { return document.querySelector(".chat-input") && document.querySelector(".chat-im.chat-editor"); } static inputMsg(msg) { //
\n 都可以换行 return document.querySelector(".chat-input").innerHTML = msg.replaceAll("\\n", "\n"); } static sendAble() { let btn = document.querySelector(".btn-v2.btn-sure-v2.btn-send"); // 删除按钮标签类名;按钮可点击 btn.classList.remove("disabled"); return btn; } static sendMsg() { // 当前标签绑定的vue组件对象, let chatFrameVueComponent = document.querySelector(".chat-im.chat-editor").__vue__; // 更新开启提交;否则提交拦截 chatFrameVueComponent.enableSubmit = true; // 赋值发送websocket的to.uid;手动触发导致uid无值,从friendId获取 chatFrameVueComponent.bossInfo$.uid = chatFrameVueComponent.bossInfo$.friendId; let element = document.querySelector(".btn-v2.btn-sure-v2.btn-send"); element.click(); } static async getMessageListTag() { return new Promise((resolve) => { document.querySelector("li.selected").click(); //等待bom渲染后获取 setTimeout(() => { const lis = document.querySelector(".user-list").querySelector("div").querySelectorAll("li"); resolve(lis); }, 100); }); } static async selectMessage(messageKey) { let messageListTag = await JobMessagePageHandler.getMessageListTag(); for (let i = 0; i < messageListTag.length; i++) { // '09月02日\n刘女士赛德勤人事行政专员\n您好,打扰了,我想和您聊聊这个职位。' // 日期\n【boss名+公司名+职位名】\n 问候语 let msgTitle = messageListTag[i].innerText; if (msgTitle.split("\n")[1] === messageKey) { return messageListTag[i].querySelector("div"); } } logger.info("本次循环消息key未检索到消息框: " + messageKey) } } class JobWordCloud { // 不应该使用分词,而应该是分句,结合上下文,自然语言处理 static filterableWorldArr = ['', ' ', ',', '?', '+', '\n', '\r', "/", '有', '的', '等', '及', '了', '和', '公司', '熟悉', '服务', '并', '同', '如', '于', '或', '到', '开发', '技术', '我们', '提供', '武汉', '经验', '为', '在', '团队', '员工', '工作', '能力', '-', '1', '2', '3', '4', '5', '6', '7', '8', '', '年', '与', '平台', '研发', '行业', "实现", "负责", "代码", "精通", "图谱", "需求", "分析", "良好", "知识", "相关", "编码", "参与", "产品", "扎实", "具备", "较", "强", "沟通", "者", "优先", "具有", "精神", "编写", "功能", "完成", "详细", "岗位职责", "包括", "解决", "应用", "性能", "调", "优", "本科", "以上学历", "基础", "责任心", "高", "构建", "合作", "能", "学习", "以上", "熟练", "问题", "优质", "运行", "工具", "方案", "根据", "业务", "类", "文档", "分配", "其他", "亿", "级", "关系", "算法", "系统", "上线", "考虑", "工程师", "华为", "自动", "驾驶", "网络", "后", "端", "云", "高质量", "承担", "重点", "难点", "攻坚", "主导", "选型", "任务", "分解", "工作量", "评估", "创造性", "过程", "中", "提升", "核心", "竞争力", "可靠性", "要求", "计算机专业", "基本功", "ee", "主流", "微", "框架", "其", "原理", "推进", "优秀", "团队精神", "热爱", "可用", "大型", "网站", "表达", "理解能力", "同事", "分享", "愿意", "接受", "挑战", "拥有", "将", "压力", "转变", "动力", "乐观", "心态", "思路清晰", "严谨", "地", "习惯", "运用", "线", "上", "独立", "处理", "熟练掌握", "至少", "一种", "常见", "脚本", "环境", "搭建", "开发工具", "人员", "讨论", "制定", "用", "相应", "保证", "质量", "说明", "领导", "包含", "节点", "存储", "检索", "api", "基于", "数据", "落地", "个性化", "场景", "支撑", "概要", "按照", "规范", "所", "模块", "评审", "编译", "调试", "单元测试", "发布", "集成", "支持", "功能测试", "测试", "结果", "优化", "持续", "改进", "配合", "交付", "出现", "任职", "资格", "编程", "型", "使用", "认真负责", "高度", "责任感", "快速", "创新", "金融", "设计", "项目", "对", "常用", "掌握", "专业", "进行", "了解", "岗位", "能够", "中间件", "以及", "开源", "理解", ")", "软件", "计算机", "架构", "一定", "缓存", "可", "解决问题", "计算机相关", "发展", "时间", "奖金", "培训", "部署", "互联网", "享受", "善于", "需要", "游戏", " ", "维护", "统招", "语言", "消息", "机制", "逻辑思维", "一", "意识", "新", "攻关", "升级", "管理", "重构", "【", "职位", "】", "成员", "好", "接口", "语句", "后台", "通用", "不", "描述", "福利", "险", "机会", "会", "人", "完善", "技术难题", "技能", "应用服务器", "配置", "协助", "或者", "组织", "现有", "迭代", "流程", "项目管理", "从", "深入", "复杂", "专业本科", "协议", "不断", "项目经理", "协作", "五", "金", "待遇", "年终奖", "各类", "节日", "带薪", "你", "智慧", "前沿技术", "常用命令", "方案设计", "基本", "积极", "产品开发", "用户", "确保", "带领", "软件系统", "撰写", "软件工程", "职责", "抗压", "积极主动", "双休", "法定", "节假日", "假", "客户", "日常", "协同", "是", "修改", "要", "软件开发", "丰富", "乐于", "识别", "风险", "合理", "服务器", "指导", "规划", "提高", "稳定性", "扩展性", "功底", "钻研", "c", "高可用性", "计算机软件", "高效", "前端", "内部", "一起", "程序", "程序开发", "计划", "按时", "数理", "及其", "集合", "正式", "劳动合同", "薪资", "丰厚", "奖励", "补贴", "免费", "体检", "每年", "调薪", "活动", "职业", "素养", "晋升", "港", "氛围", "您", "存在", "关注", "停车", "参加", "系统分析", "发现", "稳定", "自主", "实际", "开发技术", "(", "一些", "综合", "条件", "学历", "薪酬", "维", "保", "全日制", "专科", "体系结构", "协调", "出差", "自测", "周一", "至", "周五", "周末", "公积金", "准备", "内容", "部门", "满足", "兴趣", "方式", "操作", "超过", "结合", "同时", "对接", "及时", "研究", "统一", "管控", "福利待遇", "政策", "办理", "凡是", "均", "丧假", "对于", "核心技术", "安全", "服务端", "游", "电商", "零售", "下", "扩展", "负载", "信息化", "命令", "供应链", "商业", "抽象", "模型", "领域", "瓶颈", "充分", "编程语言", "自我", "但", "限于", "应用软件", "适合", "各种", "大", "前后", "复用", "执行", "流行", "app", "小", "二", "多种", "转正", "空间", "盒", "马", "长期", "成长", "间", "通讯", "全过程", "提交", "目标", "电气工程", "阅读", "严密", "电力系统", "电力", "大小", "周", "心动", "入", "职", "即", "缴纳", "签署", "绩效奖金", "评优", "专利", "论文", "职称", "加班", "带薪休假", "专项", "健康", "每周", "运动", "休闲", "不定期", "小型", "团建", "旅游", "岗前", "牛", "带队", "答疑", "解惑", "晋级", "晋升为", "管理层", "跨部门", "转岗", "地点", "武汉市", "东湖新技术开发区", "一路", "光谷", "园", "栋", "地铁", "号", "北站", "坐", "拥", "独栋", "办公楼", "环境优美", "办公", "和谐", "交通", "便利", "地铁站", "有轨电车", "公交站", "交通工具", "齐全", "凯", "默", "电气", "期待", "加入", "积极参与", "依据", "工程", "跟进", "推动", "风险意识", "owner", "保持", "积极性", "自", "研", "内", "岗", "体验", "系统维护", "可能", "在线", "沟通交流", "简洁", "清晰", "录取", "优异者", "适当", "放宽", "上浮", "必要", "后期", "软件技术", "形成", "技术成果", "调研", "分析师", "专", "含", "信息管理", "跨专业", "从业人员", "注", "安排", "交代", "书写", "做事", "细心", "好学", "可以", "公休", "年终奖金", "定期", "正规", "养老", "医疗", "生育", "工伤", "失业", "关怀", "传统", "佳节", "之际", "礼包", "团结友爱", "伙伴", "丰富多彩", "两年", "过", "连接池", "划分", "检查", "部分", "甚至", "拆解", "硕士", "年龄", "周岁", "以下", "深厚", "语法", "浓厚", "优良", "治理", "a", "力", "高级", "能看懂", "有效", "共同", "想法", "提出", "意见", "前", "最", "重要", "企业", "极好", "驻场", "并且", "表单", "交互方式", "样式", "前端开发", "遵循", "开发进度", "实战经验", "其中", "强烈", "三维", "多个", "net", "对应", "数学", "理工科", "背景", "软件设计", "模式", "方法", "动手", "按", "质", "软件产品", "严格执行", "传", "帮", "带", "任务分配", "进度", "阶段", "介入", "本科学历", "五年", "尤佳", "比较", "细致", "态度", "享", "国家", "上班时间", "基本工资", "有关", "社会保险", "公司员工", "连续", "达到", "年限", "婚假", "产假", "护理", "发展潜力", "职员", "外出", "做好", "效率", "沉淀", "网络服务", "数据分析", "查询", "规范化", "标准化", "思考", "手", "款", "成功", "卡", "牌", "slg", "更佳", "可用性", "新人", "预研", "突破", "lambda", "理念", "它", "rest", "一个", "趋势", "思路", "影响", "医疗系统", "具体", "架构师", "保证系统", "大专", "三年", "体系", "写", "医院", "遇到", "验证", "运", "保障", "基本操作", "独立思考", "技术手段", "熟知", "懂", "应用环境", "表达能力", "个人", "新能源", "汽车", "权限", "排班", "绩效", "考勤", "知识库", "全局", "搜索", "门店", "渠道", "选址", "所有", "长远", "眼光", "局限于", "逻辑", "侧", "更好", "解决方案", "针对", "建模", "定位系统", "高质", "把", "控", "攻克", "t", "必须", "组件", "基本原理", "上进心", "驱动", "适应能力", "自信", "追求", "卓越", "感兴趣", "站", "角度", "思考问题", "tob", "商业化", "售后", "毕业", "通信", "数种", "优选", "it", "课堂", "所学", "在校", "期间", "校内外", "大赛", "参", "社区", "招聘", "类库", "优等", "b", "s", "方面", "海量", "数据系统", "测试工具", "曾", "主要", "爱好", "欢迎", "洁癖", "人士", "银行", "财务", "城市", "类产品", "实施", "保障系统", "健壮性", "可读性", "rpd", "原型", "联调", "准确无误", "系统优化", "技术标准", "总体设计", "文件", "整理", "功能设计", "技术类", "写作能力", "尤其", "套件", "公安", "细分", "增加", "bug", "电子", "swing", "桌面", "认证", "台", "检测", "安全隐患", "及时发现", "修补", "上级领导", "交办", "其它", "面向对象分析", "思想", "乐于助人", "全", "栈", "共享", "经济", "信", "主管", "下达", "执行力", "技巧", "试用期", "个", "月", "适应", "快", "随时", "表现", "\u003d", "到手", "工资", "享有", "提成", "超额", "业绩", "封顶", "足够", "发展前景", "发挥", "处", "高速", "发展期", "敢", "就", "元旦", "春节", "清明", "端午", "五一", "中秋", "国庆", "婚", "病假", "商品", "导购", "增长", "互动", "营销", "面对", "不断创新", "规模化", "上下游", "各", "域", "最终", "完整", "梳理", "链路", "关键", "点", "给出", "策略", "从业", "且", "可维护性", "不仅", "短期", "更", "方向", "不错", "交互", "主动", "应急", "组长", "tl", "加", "分", "一群", "怎样", "很", "热情", "喜欢", "敬畏", "心", "坚持", "主义", "持之以恒", "自己", "收获", "重视", "每", "一位", "主观", "能动性", "同学", "给予", "为此", "求贤若渴", "干货", "满满", "战斗", "大胆", "互相", "信任", "互相帮助", "生活", "里", "嗨", "皮", "徒步", "桌", "轰", "趴", "聚餐", "应有尽有" ] static numberRegex = /^[0-9]+$/ static splitChar = " " static participleUrl = "https://www.tl.beer/api/v1/fenci" static participle(text) { return new Promise((resolve, reject) => { TampermonkeyApi.GMXmlHttpRequest({ method: 'POST', timeout: 5000, url: JobWordCloud.participleUrl, data: "cont=" + encodeURIComponent(text) + "&cixin=false&model=false", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, onload: function (response) { if (response.status !== 200) { logger.error("分词状态码不是200", response.responseText) return reject(response.responseText) } return resolve(JSON.parse(response.responseText).data.split(JobWordCloud.splitChar)) }, onerror: function (error) { logger.error("分词出错", error) reject(error) } }); }) } static buildWord(wordArr) { // {"word1":1, "word2":4} let weightMap = {}; for (let i = 0; i < wordArr.length; i++) { let str = wordArr[i]; if (JobWordCloud.filterableWorldArr.includes(str)) { continue; } if (JobWordCloud.numberRegex.test(str)) { continue; } if (str in weightMap) { weightMap[str] = weightMap[str] + 1; continue } weightMap[str] = 1; } // 将对象转换为二维数组并排序: [['word1', 2], ['word2', 4]] let weightWordArr = JobWordCloud.sortByValue(Object.entries(weightMap)); return JobWordCloud.cutData(weightWordArr) } static cutData(weightWordArr) { return weightWordArr } static generateWorldCloudImage(canvasTagId, weightWordArr) { // 词云图的配置选项 let options = { tooltip: { show: true, formatter: function (item) { return item[0] + ': ' + item[1] } }, list: weightWordArr, // 网格尺寸 //gridSize: 10, // 权重系数 weightFactor: 2, // 字体 fontFamily: 'Finger Paint, cursive, sans-serif', // 字体颜色,也可以指定特定颜色值 //color: '#26ad7e', color: 'random-dark', // 旋转比例 // rotateRatio: 0.2, // 背景颜色 backgroundColor: 'white', // 形状 //shape: 'square', shape: 'circle', ellipticity: 1, // 随机排列词语 shuffle: true, // 不绘制超出容器边界的词语 drawOutOfBound: false }; // WordCloud(document.getElementById(canvasTagId), options); const wc = new Js2WordCloud(document.getElementById(canvasTagId)); wc.setOption(options) } static getKeyWorldArr(twoArr) { let worldArr = [] for (let i = 0; i < twoArr.length; i++) { let world = twoArr[i][0]; worldArr.push(world) } return worldArr; } static sortByValue(arr, order = 'desc') { if (order === 'asc') { return arr.sort((a, b) => a[1] - b[1]); } else if (order === 'desc') { return arr.sort((a, b) => b[1] - a[1]); } else { throw new Error('Invalid sort key. Use "asc" or "desc".'); } } } GM_registerMenuCommand("切换Ck", async () => { let value = GM_getValue("ck_list") || []; GM_cookie("list", {}, async (list, error) => { if (error === undefined) { console.log(list, value); // 储存覆盖老的值 GM_setValue("ck_list", list); // 先清空 再设置 for (let i = 0; i < list.length; i++) { list[i].url = window.location.origin; await GM_cookie("delete", list[i]); } if (value.length) { // 循环set for (let i = 0; i < value.length; i++) { value[i].url = window.location.origin; await GM_cookie("set", value[i]); } } if (GM_getValue("ck_cur", "") === "") { GM_setValue("ck_cur", "_"); } else { GM_setValue("ck_cur", ""); } window.location.reload(); // window.alert("手动刷新~"); } else { window.alert("你当前版本可能不支持Ck操作,错误代码:" + error); } }); }); GM_registerMenuCommand("清除当前Ck", () => { if (GM_getValue("ck_cur", "") === "_") { GM_setValue("ck_cur", ""); } GM_cookie("list", {}, async (list, error) => { if (error === undefined) { // 清空 for (let i = 0; i < list.length; i++) { list[i].url = window.location.origin; // console.log(list[i]); await GM_cookie("delete", list[i]); } window.location.reload(); } else { window.alert("你当前版本可能不支持Ck操作,错误代码:" + error); } }); }); GM_registerMenuCommand("清空所有存储!", async () => { if (confirm("将清空脚本全部的设置!!")) { const asyncKeys = await GM_listValues(); for (let index in asyncKeys) { if (!asyncKeys.hasOwnProperty(index)) { continue; } console.log(asyncKeys[index]); await GM_deleteValue(asyncKeys[index]); } window.alert("OK!"); } }); (function () { const list_url = "web/geek/job"; const recommend_url = "web/geek/recommend"; const message_url = "web/geek/chat"; if (document.URL.includes(list_url) || document.URL.includes(recommend_url)) { window.addEventListener("load", () => { new JobListPageHandler() }); } else if (document.URL.includes(message_url) && parent?.document?.getElementById('msgIframe')) { window.addEventListener("load", () => { // jobListPage内部的 msgIframe才注册 new JobMessagePageHandler(); }); } })();