// ==UserScript==
// @name BOSS直聘批量投递助手
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 自动批量投递职位,处理弹窗,支持关键词跳过
// @author chenni666
// @match https://www.zhipin.com/web/geek/jobs?*
// @icon https://www.zhipin.com/favicon.ico
// @grant GM_addStyle
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @license GNU AGPLv3
// ==/UserScript==
(function() {
'use strict';
// 添加自定义样式
GM_addStyle(`
.batch-apply-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: 300px;
max-height: 85vh;
overflow-y: auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
}
.batch-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.batch-title {
font-size: 16px;
font-weight: bold;
color: #1a1a1a;
}
.batch-controls {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.batch-btn {
flex: 1;
padding: 6px 10px;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
font-size: 13px;
}
.start-btn {
background: #00a6ff;
color: white;
}
.start-btn:hover {
background: #0088cc;
}
.start-btn:disabled {
background: #b3e0ff;
cursor: not-allowed;
}
.pause-btn {
background: #ff9500;
color: white;
}
.pause-btn:hover {
background: #e08600;
}
.pause-btn:disabled {
background: #ffd699;
cursor: not-allowed;
}
.stop-btn {
background: #ff3b30;
color: white;
}
.stop-btn:hover {
background: #d63026;
}
.stop-btn:disabled {
background: #ffb3b0;
cursor: not-allowed;
}
.progress-container {
margin-bottom: 10px;
}
.progress-bar {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00a6ff, #4cc9f0);
width: 0%;
transition: width 0.3s;
}
.progress-text {
font-size: 12px;
color: #666;
margin-top: 4px;
text-align: center;
}
.status-log {
height: 100px;
overflow-y: auto;
border: 1px solid #eee;
border-radius: 4px;
padding: 8px;
font-size: 12px;
background: #f9f9f9;
margin-bottom: 8px;
}
.log-entry {
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px dashed #eee;
line-height: 1.4;
}
.log-entry:last-child {
border-bottom: none;
margin-bottom: 0;
}
.log-success {
color: #28a745;
}
.log-error {
color: #dc3545;
}
.log-info {
color: #17a2b8;
}
.log-warning {
color: #ff9500;
}
.stats-container {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 12px;
color: #666;
}
.stats-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stats-value {
font-weight: bold;
font-size: 14px;
color: #00a6ff;
}
.settings-toggle {
color: #00a6ff;
cursor: pointer;
font-size: 13px;
text-align: right;
margin-bottom: 5px;
}
.settings-panel {
padding-top: 8px;
border-top: 1px solid #eee;
max-height: 200px;
overflow-y: auto;
}
.setting-item {
margin-bottom: 8px;
}
.setting-label {
display: block;
margin-bottom: 3px;
font-size: 12px;
color: #666;
}
.setting-input {
width: 100%;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
box-sizing: border-box;
}
.setting-checkbox {
margin-right: 8px;
}
.save-notification {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #28a745;
color: white;
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
}
.save-notification.show {
opacity: 1;
}
`);
// 创建控制面板
const container = document.createElement('div');
container.className = 'batch-apply-container';
container.innerHTML = `
<div class="batch-header">
<div class="batch-title">批量投递助手</div>
</div>
<div class="batch-controls">
<button class="batch-btn start-btn" id="start-btn">开始投递</button>
<button class="batch-btn pause-btn" id="pause-btn" disabled>暂停</button>
<button class="batch-btn stop-btn" id="stop-btn" disabled>停止</button>
</div>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-text" id="progress-text">准备就绪</div>
</div>
<div class="status-log" id="status-log"></div>
<div class="stats-container">
<div class="stats-item">
<div>总职位</div>
<div class="stats-value" id="total-jobs">0</div>
</div>
<div class="stats-item">
<div>已投递</div>
<div class="stats-value" id="applied-jobs">0</div>
</div>
<div class="stats-item">
<div>成功率</div>
<div class="stats-value" id="success-rate">0%</div>
</div>
</div>
<div class="settings-toggle" id="settings-toggle">▼ 高级设置</div>
<div class="settings-panel" id="settings-panel" style="display:none;">
<div class="setting-item">
<label class="setting-label">职位间隔时间(毫秒)</label>
<input type="number" class="setting-input" id="interval-time" value="2000">
</div>
<div class="setting-item">
<label class="setting-label">弹窗等待时间(毫秒)</label>
<input type="number" class="setting-input" id="popup-wait" value="1500">
</div>
<div class="setting-item">
<label class="setting-label">最大重试次数</label>
<input type="number" class="setting-input" id="max-retry" value="3">
</div>
<div class="setting-item">
<label>
<input type="checkbox" class="setting-checkbox" id="auto-close-popup" checked>
自动关闭弹窗
</label>
</div>
<div class="setting-item">
<label>
<input type="checkbox" class="setting-checkbox" id="skip-applied" checked>
跳过已投递职位
</label>
</div>
<!-- 新增跳过关键词设置 -->
<div class="setting-item">
<label class="setting-label">职位名称跳过词(逗号分隔)</label>
<input type="text" class="setting-input" id="title-keywords" placeholder="例如:外包,驻场,销售">
</div>
<div class="setting-item">
<label class="setting-label">职业描述跳过词(逗号分隔)</label>
<textarea class="setting-input" id="skip-keywords" rows="2" placeholder="例如:外包,驻场,销售,兼职"></textarea>
</div>
</div>
`;
document.body.appendChild(container);
// 状态变量
let jobItems = [];
let currentIndex = 0;
let isProcessing = false;
let isPaused = false;
let totalJobs = 0;
let appliedCount = 0;
let successCount = 0;
let retryCount = 0;
let currentTimeoutId = null; // 当前延时任务ID
const statusLog = document.getElementById('status-log');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
const totalJobsEl = document.getElementById('total-jobs');
const appliedJobsEl = document.getElementById('applied-jobs');
const successRateEl = document.getElementById('success-rate');
const startBtn = document.getElementById('start-btn');
const pauseBtn = document.getElementById('pause-btn');
const stopBtn = document.getElementById('stop-btn');
const settingsToggle = document.getElementById('settings-toggle');
const settingsPanel = document.getElementById('settings-panel');
const intervalTimeInput = document.getElementById('interval-time');
const popupWaitInput = document.getElementById('popup-wait');
const maxRetryInput = document.getElementById('max-retry');
const autoClosePopupCheck = document.getElementById('auto-close-popup');
const skipAppliedCheck = document.getElementById('skip-applied');
// 新增DOM元素引用
const skipKeywordsInput = document.getElementById('skip-keywords');
const titleKeywordsInput = document.getElementById('title-keywords');
// 加载设置 - 增加关键词设置
function loadSettings() {
intervalTimeInput.value = GM_getValue('intervalTime', 2000);
popupWaitInput.value = GM_getValue('popupWait', 1500);
maxRetryInput.value = GM_getValue('maxRetry', 3);
autoClosePopupCheck.checked = GM_getValue('autoClosePopup', true);
skipAppliedCheck.checked = GM_getValue('skipApplied', true);
skipKeywordsInput.value = GM_getValue('skipKeywords', '');
titleKeywordsInput.value = GM_getValue('titleKeywords', '');
}
// 保存设置 - 增加关键词设置
function saveSettings() {
GM_setValue('intervalTime', parseInt(intervalTimeInput.value));
GM_setValue('popupWait', parseInt(popupWaitInput.value));
GM_setValue('maxRetry', parseInt(maxRetryInput.value));
GM_setValue('autoClosePopup', autoClosePopupCheck.checked);
GM_setValue('skipApplied', skipAppliedCheck.checked);
GM_setValue('skipKeywords', skipKeywordsInput.value);
GM_setValue('titleKeywords', titleKeywordsInput.value);
// 显示保存成功提醒
showSaveNotification();
}
// 显示保存设置提醒
function showSaveNotification() {
// 移除已存在的提醒
const existingNotification = document.querySelector('.save-notification');
if (existingNotification) {
existingNotification.remove();
}
// 创建新的提醒
const notification = document.createElement('div');
notification.className = 'save-notification';
notification.textContent = '设置已保存';
document.body.appendChild(notification);
// 显示动画
setTimeout(() => {
notification.classList.add('show');
}, 10);
// 2秒后隐藏并移除
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}, 2000);
}
// 添加日志
function addLog(message, type = 'info') {
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
statusLog.appendChild(logEntry);
statusLog.scrollTop = statusLog.scrollHeight;
}
// 更新统计信息
function updateStats() {
totalJobsEl.textContent = totalJobs;
appliedJobsEl.textContent = appliedCount;
const rate = totalJobs > 0 ? Math.round((successCount / totalJobs) * 100) : 0;
successRateEl.textContent = `${rate}%`;
}
// 更新进度
function updateProgress() {
const percentage = totalJobs > 0 ? Math.round((appliedCount / totalJobs) * 100) : 0;
progressFill.style.width = `${percentage}%`;
progressText.textContent = totalJobs > 0 ?
`${appliedCount}/${totalJobs} (${percentage}%)` : '准备就绪';
}
// 获取职位列表
function getJobList() {
return Array.from(document.querySelectorAll('.job-card-box'));
}
// 检查职位是否已投递
function isJobApplied(jobItem) {
const appliedIndicator = jobItem.querySelector('.job-applied');
return appliedIndicator !== null;
}
// 检查职位是否包含关键词
function containsKeywords(jobItem, jobName = null) {
// 获取设置的关键词列表
const skipKeywords = GM_getValue('skipKeywords', '').split(',').map(k => k.trim()).filter(k => k);
const titleKeywords = GM_getValue('titleKeywords', '').split(',').map(k => k.trim()).filter(k => k);
// 检查职位名称中的关键词
const jobTitleElem = jobItem.querySelector('.job-title');
const jobNameText = jobTitleElem ? jobTitleElem.textContent : '';
if (titleKeywords.length > 0 && jobNameText) {
const lowerJobName = jobNameText.toLowerCase();
for (const keyword of titleKeywords) {
if (keyword && lowerJobName.includes(keyword.toLowerCase())) {
return {
match: true,
keyword: keyword,
type: '职位名称'
};
}
}
}
// 检查职位描述中的关键词(需要加载详细页面)
if (!jobName) return {match: false};
const jobDescElem = document.querySelector('.job-detail-body .desc');
if (jobDescElem && skipKeywords.length > 0) {
const jobDescText = jobDescElem.textContent.toLowerCase();
for (const keyword of skipKeywords) {
if (keyword && jobDescText.includes(keyword.toLowerCase())) {
return {
match: true,
keyword: keyword,
type: '职位描述'
};
}
}
}
return {match: false};
}
// 点击职位项 - 增加关键词检查
function clickJobItem(index) {
if (index >= jobItems.length) {
addLog('所有职位已处理完毕', 'success');
isProcessing = false;
resetButtons();
return;
}
const jobItem = jobItems[index];
// 检查是否跳过已投递职位
if (skipAppliedCheck.checked && isJobApplied(jobItem)) {
addLog(`跳过已投递职位 ${index + 1}`, 'info');
appliedCount++;
updateProgress();
updateStats();
currentTimeoutId = setTimeout(() => processNext(), 500);
return;
}
jobItem.click();
addLog(`已选择职位 ${index + 1}/${jobItems.length}`);
// 获取职位名称用于检查
const jobNameElem = document.querySelector('.job-detail-info .job-name');
const jobName = jobNameElem ? jobNameElem.textContent.trim() : null;
// 设置延迟检查关键词
currentTimeoutId = setTimeout(() => {
// 执行关键词检查
const keywordCheck = containsKeywords(jobItem, jobName);
if (keywordCheck.match) {
addLog(`跳过职位 ${index + 1} (包含${keywordCheck.type}关键词: ${keywordCheck.keyword})`, 'warning');
appliedCount++;
updateProgress();
updateStats();
processNext();
return;
}
// 如果没有关键词匹配,继续投递流程
tryApply(index);
}, parseInt(popupWaitInput.value) / 2); // 提前一半时间检查关键词
}
// 尝试投递
function tryApply(index) {
// 查找立即沟通按钮
const applyBtn = document.querySelector('.op-btn-chat');
if (applyBtn && applyBtn.textContent.includes('立即沟通')) {
applyBtn.click();
addLog(`正在投递职位 ${index + 1}...`, 'info');
// 处理弹窗
currentTimeoutId = setTimeout(() => handlePopup(index), parseInt(popupWaitInput.value));
} else {
addLog(`职位 ${index + 1} 无法投递(按钮不存在或已投递)`, 'warning');
appliedCount++;
updateProgress();
updateStats();
processNext();
}
}
// 处理弹窗
function handlePopup(index) {
const popup = document.querySelector('.greet-boss-container');
const stayButton = document.querySelector('.cancel-btn');
if (popup && stayButton && autoClosePopupCheck.checked) {
stayButton.click();
addLog(`成功投递职位 ${index + 1},已关闭弹窗`, 'success');
successCount++;
} else if (popup) {
addLog(`职位 ${index + 1} 投递成功,但未关闭弹窗`, 'warning');
successCount++;
} else {
// 未检测到弹窗,可能是投递失败
if (retryCount < parseInt(maxRetryInput.value)) {
retryCount++;
addLog(`职位 ${index + 1} 投递未确认,重试中 (${retryCount}/${maxRetryInput.value})`, 'warning');
currentTimeoutId = setTimeout(() => tryApply(index), 1000);
return;
} else {
addLog(`职位 ${index + 1} 投递失败,达到最大重试次数`, 'error');
}
}
appliedCount++;
updateProgress();
updateStats();
retryCount = 0;
processNext();
}
// 处理下一个职位
function processNext() {
if (isPaused) return;
currentIndex++;
if (currentIndex < jobItems.length) {
currentTimeoutId = setTimeout(() => clickJobItem(currentIndex), parseInt(intervalTimeInput.value));
} else {
addLog('批量投递完成!', 'success');
isProcessing = false;
resetButtons();
GM_notification({
title: 'BOSS直聘批量投递完成',
text: `成功投递 ${successCount}/${totalJobs} 个职位`,
timeout: 5000
});
}
}
// 开始批量投递
function startBatchApply() {
if (isProcessing) return;
// 清除可能存在的延时任务
if (currentTimeoutId) {
clearTimeout(currentTimeoutId);
currentTimeoutId = null;
}
jobItems = getJobList();
if (jobItems.length === 0) {
addLog('未找到职位列表,请刷新页面重试', 'error');
return;
}
totalJobs = jobItems.length;
currentIndex = 0;
appliedCount = 0;
successCount = 0;
retryCount = 0;
isProcessing = true;
isPaused = false;
addLog(`开始批量投递,共 ${jobItems.length} 个职位`);
updateProgress();
updateStats();
// 更新按钮状态
startBtn.disabled = true;
pauseBtn.disabled = false;
stopBtn.disabled = false;
clickJobItem(currentIndex);
}
// 暂停投递
function pauseBatchApply() {
if (!isProcessing) return;
isPaused = true;
addLog('投递已暂停');
pauseBtn.textContent = '继续';
pauseBtn.removeEventListener('click', pauseBatchApply);
pauseBtn.addEventListener('click', resumeBatchApply);
}
// 继续投递
function resumeBatchApply() {
if (!isProcessing) return;
isPaused = false;
addLog('继续投递');
pauseBtn.textContent = '暂停';
pauseBtn.removeEventListener('click', resumeBatchApply);
pauseBtn.addEventListener('click', pauseBatchApply);
processNext();
}
// 停止投递
function stopBatchApply() {
if (!isProcessing) return;
// 清除当前延时任务
if (currentTimeoutId) {
clearTimeout(currentTimeoutId);
currentTimeoutId = null;
}
isProcessing = false;
isPaused = false;
addLog('投递已停止');
addLog(`本次投递统计: 处理了 ${appliedCount}/${totalJobs} 个职位,成功 ${successCount} 个`);
resetButtons();
}
// 重置按钮状态
function resetButtons() {
startBtn.disabled = false;
pauseBtn.disabled = true;
stopBtn.disabled = true;
pauseBtn.textContent = '暂停';
pauseBtn.removeEventListener('click', resumeBatchApply);
pauseBtn.addEventListener('click', pauseBatchApply);
// 清除延时任务
if (currentTimeoutId) {
clearTimeout(currentTimeoutId);
currentTimeoutId = null;
}
}
// 初始化
function init() {
// 加载设置
loadSettings();
// 添加事件监听
startBtn.addEventListener('click', startBatchApply);
pauseBtn.addEventListener('click', pauseBatchApply);
stopBtn.addEventListener('click', stopBatchApply);
// 设置面板切换
settingsToggle.addEventListener('click', () => {
if (settingsPanel.style.display === 'none') {
settingsPanel.style.display = 'block';
settingsToggle.textContent = '▲ 收起设置';
} else {
settingsPanel.style.display = 'none';
settingsToggle.textContent = '▼ 高级设置';
}
});
// 设置变更保存 - 增加关键词设置
intervalTimeInput.addEventListener('change', saveSettings);
popupWaitInput.addEventListener('change', saveSettings);
maxRetryInput.addEventListener('change', saveSettings);
autoClosePopupCheck.addEventListener('change', saveSettings);
skipAppliedCheck.addEventListener('change', saveSettings);
skipKeywordsInput.addEventListener('change', saveSettings);
titleKeywordsInput.addEventListener('change', saveSettings);
// 初始日志
addLog('脚本已加载,准备就绪');
addLog('点击"开始投递"按钮开始批量投递');
addLog('注意:请确保在BOSS直聘职位列表页面使用本脚本');
}
// 启动初始化
init();
})();