// ==UserScript==
// @name 招聘职位助手(51job/Boss/智联)
// @namespace https://we.51job.com
// @version 1.6.5
// @description 管理屏蔽公司和职位关键词;支持 51job、Boss 直聘和智联招聘。
// @author WorkBuddy
// @match https://we.51job.com/*
// @match https://*.51job.com/*
// @match https://www.zhipin.com/*
// @match https://*.zhipin.com/*
// @match https://www.zhaopin.com/*
// @match https://*.zhaopin.com/*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/* =====================================================================
* 站点配置
* ===================================================================== */
const SITES = {
'51job.com': {
card: [
'.joblist-item-job[sensorsname="JobShortExposure"]',
'.joblist-item-job.sensors_exposure',
'.joblist-item-job',
'li.joblist',
'.j_joblist > div',
'.joblist-box .item',
'.job-card',
],
company: [
'.joblist-item-right .cname',
'a.comp .cname',
'.cname',
'.e_com',
'[data-company]',
'[data-corp]',
'[class*="company" i]',
'[class*="corp" i]',
].join(','),
title: [
'.joblist-item-jobname .jname',
'.joblist-item-jobname [title]',
'.jname',
'[class*="jobname" i]',
'[class*="job-name" i]',
'[class*="job-title" i]',
'[class*="title" i]',
'h3',
'h2',
].join(','),
link: [
'a[href*="jobid"]',
'a[href*="jobs.51job.com"][href*=".html"]:not(.comp)',
'a[href*="/job/"]',
'a[href*="position"]',
'a[href*="post"]',
].join(','),
},
'zhipin.com': {
card: [
'.job-card-wrap',
],
company: [
'.job-card-footer a[href*="/gongsi/"]',
'a[href*="/gongsi/"]',
'.company-name',
'[class*="company-name" i]',
].join(','),
title: [
'.job-title',
'[class*="job-title" i]',
'.job-name',
].join(','),
link: [
'a[href*="job_detail"]',
].join(','),
},
'zhaopin.com': {
card: [
'.joblist-box__item',
'.joblist-box__iteminfo',
'[class*="joblist-box__item"]',
'[class*="job-card"]',
],
company: [
'.company-name',
'[class*="company-name" i]',
'[class*="companyname" i]',
'a[href*="/companydetail/"]',
'a[href*="company"]',
].join(','),
title: [
'.job-name',
'[class*="job-name" i]',
'[class*="jobname" i]',
'a[href*="/jobdetail/"]',
'a[href*="jobs.zhaopin.com"]',
].join(','),
link: [
'a[href*="/jobdetail/"]',
'a[href*="jobs.zhaopin.com"]',
'a[href*="jobDetail"]',
].join(','),
},
};
function pickSite() {
const hostname = location.hostname;
for (const domain of Object.keys(SITES)) {
if (
hostname === domain ||
hostname.endsWith(`.${domain}`)
) {
return SITES[domain];
}
}
return SITES['51job.com'];
}
const SITE = pickSite();
const IS_51JOB =
location.hostname === '51job.com' ||
location.hostname.endsWith('.51job.com');
const IS_BOSS =
location.hostname === 'zhipin.com' ||
location.hostname.endsWith('.zhipin.com');
const IS_ZHILIAN =
location.hostname === 'zhaopin.com' ||
location.hostname.endsWith('.zhaopin.com');
const CONFIG = {
STORAGE_KEY: 'wj_blocked_companies',
TITLE_STORAGE_KEY: 'wj_blocked_job_titles',
BOSS_CONTACTED_KEY: 'wj_boss_contacted_job_ids',
POSITION_KEY: 'wj_fab_pos',
EXACT_KEY: 'wj_exact_match',
DEFAULT_EXACT_MATCH: false,
MAX_LOG: 200,
SCAN_DELAY: 180,
FAB_SIZE: 48,
PANEL_WIDTH: 350,
DELIVERY_MIN_DELAY: 800,
DELIVERY_MAX_DELAY: 2000,
};
/* =====================================================================
* 存储
* ===================================================================== */
const hasGM =
typeof GM_getValue === 'function' &&
typeof GM_setValue === 'function';
function load(key, defaultValue) {
try {
if (hasGM) {
const value = GM_getValue(key, null);
if (value !== null && value !== undefined) {
return typeof value === 'string'
? JSON.parse(value)
: value;
}
}
} catch {
// GM 存储读取失败时回退至 localStorage。
}
try {
const raw = localStorage.getItem(key);
if (raw !== null) {
return JSON.parse(raw);
}
} catch {
// localStorage 不可用时返回默认值。
}
return defaultValue;
}
function save(key, value) {
const raw = JSON.stringify(value);
try {
if (hasGM) {
GM_setValue(key, raw);
}
} catch {
// GM 存储失败不影响 localStorage。
}
try {
localStorage.setItem(key, raw);
} catch {
// 某些隐私模式下 localStorage 可能不可用。
}
}
function loadBlockedCompanies() {
const value = load(CONFIG.STORAGE_KEY, []);
if (!Array.isArray(value)) {
return [];
}
return Array.from(
new Set(
value
.filter((item) => typeof item === 'string')
.map((item) => item.trim())
.filter(Boolean),
),
);
}
function loadBlockedTitles() {
const value = load(CONFIG.TITLE_STORAGE_KEY, []);
if (!Array.isArray(value)) {
return [];
}
return Array.from(
new Set(
value
.filter((item) => typeof item === 'string')
.map((item) => item.trim())
.filter(Boolean),
),
);
}
function loadBossContactedJobIds() {
const value = load(CONFIG.BOSS_CONTACTED_KEY, []);
return Array.isArray(value)
? value.filter((item) => typeof item === 'string' && item)
: [];
}
let blocked = loadBlockedCompanies();
let blockedTitles = loadBlockedTitles();
const bossContactedJobIds = new Set(loadBossContactedJobIds());
let exactMatch = Boolean(
load(
CONFIG.EXACT_KEY,
CONFIG.DEFAULT_EXACT_MATCH,
),
);
const filteredLog = [];
const loggedJobs = new Set();
let statDetected = 0;
let statHidden = 0;
let scanning = false;
let scanQueued = false;
let scanTimer = null;
const deliveryState = {
running: false,
stopRequested: false,
total: 0,
completed: 0,
triggered: 0,
skipped: 0,
currentTitle: '',
currentCompany: '',
message: '',
};
let lastBossPopupDiagnostic = null;
/* =====================================================================
* 文本及职位信息提取
* ===================================================================== */
function normalizeVisibleText(value) {
return String(value || '')
.normalize('NFKC')
.replace(/\s+/g, ' ')
.trim();
}
function normalizeCompanyName(value) {
return normalizeVisibleText(value)
.replace(/\s+/g, '')
.replace(/[((][^()()]{0,30}[))]$/u, '')
.trim();
}
function getElementText(element) {
if (!element) {
return '';
}
const title = element.getAttribute?.('title');
if (title && title.trim()) {
return normalizeVisibleText(title);
}
return normalizeVisibleText(element.textContent);
}
function extractText(card, selector) {
if (!selector) {
return '';
}
return getElementText(card.querySelector(selector));
}
const COMPANY_SUFFIX =
/(公司|集团|股份|企业|有限责任|有限合伙|合作社|研究院|研究所|银行|保险|证券|基金|事务所|商行|商场|超市|酒店|饭店|学校|学院|医院|诊所|门诊|出版社|报社|电视台|电台|工作室|个体户|中心|厂|社|行|店)$/;
function depthOf(element) {
let depth = 0;
let current = element;
while (current && current !== document.body) {
depth++;
current = current.parentElement;
}
return depth;
}
function extractTitle(card) {
let title = extractText(card, SITE.title);
if (!title) {
title = getElementText(
card.querySelector(SITE.link),
);
}
return title || '(未知职位)';
}
function extractCompany(card) {
// 51job 新版页面的 .cname title 属性最可靠。
const hint = card.querySelector(SITE.company);
const hintedCompany = getElementText(hint);
if (hintedCompany) {
return hintedCompany;
}
// 页面结构变化时使用启发式识别。
const title = normalizeCompanyName(
extractTitle(card),
);
const walker = document.createTreeWalker(
card,
NodeFilter.SHOW_TEXT,
);
let node;
let best = '';
let bestScore = Number.NEGATIVE_INFINITY;
while ((node = walker.nextNode())) {
const raw = normalizeVisibleText(
node.textContent,
);
const normalized = normalizeCompanyName(raw);
if (
normalized.length < 2 ||
normalized.length > 50 ||
normalized === title
) {
continue;
}
const parent = node.parentElement;
if (!parent) {
continue;
}
const className =
typeof parent.className === 'string'
? parent.className
: '';
const hasSuffix =
COMPANY_SUFFIX.test(normalized);
const hasCompanyHint =
/(?:^|\s)(?:cname|comp|company-name)(?:\s|$)|ent|corp|e_com/i.test(
className,
);
if (!hasSuffix && !hasCompanyHint) {
continue;
}
const score =
(hasSuffix ? 50 : 0) +
(hasCompanyHint ? 35 : 0) -
normalized.length -
depthOf(parent);
if (score > bestScore) {
bestScore = score;
best = raw;
}
}
return best;
}
function matchCompany(card) {
const company = normalizeCompanyName(
extractCompany(card),
);
if (!company) {
return null;
}
for (const blockedCompany of blocked) {
const normalizedBlocked =
normalizeCompanyName(blockedCompany);
if (!normalizedBlocked) {
continue;
}
if (exactMatch) {
if (company === normalizedBlocked) {
return blockedCompany;
}
continue;
}
if (company.includes(normalizedBlocked)) {
return blockedCompany;
}
}
return null;
}
function matchJobTitle(card) {
const title = normalizeVisibleText(
extractTitle(card),
).toLocaleLowerCase();
if (!title) {
return null;
}
for (const blockedTitle of blockedTitles) {
const keyword = normalizeVisibleText(
blockedTitle,
).toLocaleLowerCase();
if (keyword && title.includes(keyword)) {
return blockedTitle;
}
}
return null;
}
/* =====================================================================
* 职位卡片识别
* ===================================================================== */
function inferCard(linkElement) {
let node = linkElement.parentElement;
let depth = 0;
while (
node &&
node !== document.body &&
depth < 12
) {
if (
node.matches?.(
'.joblist-item-job, .job-card-wrapper, .job-card-wrap, li.job-card',
)
) {
return node;
}
const parent = node.parentElement;
if (!parent) {
break;
}
const currentLinkCount =
node.querySelectorAll?.(SITE.link).length || 0;
const parentLinkCount =
parent.querySelectorAll?.(SITE.link).length || 0;
if (
currentLinkCount > 0 &&
parentLinkCount > currentLinkCount
) {
return node;
}
node = parent;
depth++;
}
return (
linkElement.closest(
'.joblist-item-job, .job-card-wrapper, .job-card-wrap, li.job-card, li, article',
) ||
linkElement
);
}
function removeDuplicateCards(cards) {
const uniqueCards = Array.from(new Set(cards));
// 移除包含其他职位卡片的外层容器。
return uniqueCards.filter(
(card) =>
!uniqueCards.some(
(other) =>
other !== card &&
card.contains(other),
),
);
}
function getCards() {
for (const selector of SITE.card) {
let elements;
try {
elements = Array.from(
document.querySelectorAll(selector),
);
} catch {
continue;
}
if (elements.length > 0) {
return removeDuplicateCards(elements);
}
}
const links = document.querySelectorAll(
SITE.link,
);
const inferredCards = [];
for (const link of links) {
const card = inferCard(link);
if (card) {
inferredCards.push(card);
}
}
return removeDuplicateCards(inferredCards);
}
function getJobIdentity(card) {
const sensorsData =
card.getAttribute('sensorsdata');
if (sensorsData) {
try {
const parsed = JSON.parse(sensorsData);
if (parsed?.jobId) {
return `51job:${parsed.jobId}`;
}
} catch {
const match = sensorsData.match(
/["']?jobId["']?\s*:\s*["']?(\d+)/i,
);
if (match) {
return `51job:${match[1]}`;
}
}
}
const link = card.querySelector(SITE.link);
const href = link?.href || '';
if (href) {
return `link:${href}`;
}
return [
'fallback',
normalizeCompanyName(extractCompany(card)),
normalizeVisibleText(extractTitle(card)),
].join(':');
}
/* =====================================================================
* HTML 转义
* ===================================================================== */
function escapeHtml(value) {
return String(value).replace(
/[&<>"']/g,
(character) =>
({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
})[character],
);
}
function escapeAttribute(value) {
return escapeHtml(value);
}
/* =====================================================================
* 样式
* ===================================================================== */
const STYLE = `
.wj-fab {
position: fixed;
z-index: 2147483647;
right: 18px;
bottom: 18px;
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #ff6a3d, #ff3d3d);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
user-select: none;
box-shadow: 0 4px 14px rgba(255, 61, 61, 0.45);
font-size: 20px;
touch-action: none;
}
.wj-fab:active {
cursor: grabbing;
}
.wj-fab .wj-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 18px;
height: 18px;
padding: 0 4px;
border-radius: 9px;
background: #222;
color: #fff;
font-size: 11px;
line-height: 18px;
text-align: center;
border: 2px solid #fff;
box-sizing: content-box;
}
.wj-panel {
position: fixed;
z-index: 2147483647;
width: 350px;
max-height: 84vh;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25);
display: none;
flex-direction: column;
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"PingFang SC",
"Microsoft YaHei",
sans-serif;
font-size: 13px;
color: #222;
overflow: hidden;
border: 1px solid #eee;
}
.wj-panel.wj-show {
display: flex;
}
.wj-head {
background: linear-gradient(135deg, #ff6a3d, #ff3d3d);
color: #fff;
padding: 12px 14px;
font-weight: 600;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
.wj-head .wj-close {
cursor: pointer;
font-size: 18px;
line-height: 1;
opacity: 0.9;
}
.wj-body {
padding: 12px 14px;
overflow-y: auto;
}
.wj-row {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.wj-row input {
flex: 1;
min-width: 0;
padding: 7px 9px;
border: 1px solid #ddd;
border-radius: 7px;
outline: none;
font-size: 13px;
}
.wj-row input:focus {
border-color: #ff6a3d;
}
.wj-btn {
padding: 7px 12px;
border: none;
border-radius: 7px;
background: #ff6a3d;
color: #fff;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
}
.wj-btn:hover {
background: #ff5530;
}
.wj-btn.wj-ghost {
background: #f1f1f1;
color: #555;
}
.wj-btn.wj-ghost:hover {
background: #e8e8e8;
}
.wj-sec-title {
font-weight: 600;
margin: 6px 0 8px;
color: #444;
display: flex;
justify-content: space-between;
align-items: center;
}
.wj-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: #fff0ec;
color: #ff3d3d;
border: 1px solid #ffd5cc;
padding: 4px 8px;
border-radius: 14px;
margin: 0 6px 6px 0;
font-size: 12px;
}
.wj-chip .wj-x {
cursor: pointer;
font-weight: 700;
}
.wj-empty {
color: #aaa;
font-size: 12px;
padding: 6px 0;
}
.wj-log {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
padding: 8px 0;
border-bottom: 1px dashed #eee;
}
.wj-log .wj-info {
min-width: 0;
overflow: hidden;
}
.wj-log .wj-co {
color: #ff3d3d;
font-weight: 600;
}
.wj-log .wj-ti {
color: #333;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 190px;
}
.wj-log .wj-time {
color: #bbb;
font-size: 11px;
}
.wj-log .wj-unblock {
background: none;
border: 1px solid #ddd;
color: #888;
border-radius: 6px;
padding: 2px 7px;
cursor: pointer;
font-size: 11px;
white-space: nowrap;
}
.wj-log .wj-unblock:hover {
border-color: #ff6a3d;
color: #ff6a3d;
}
.wj-foot {
padding: 10px 14px;
border-top: 1px solid #eee;
display: flex;
gap: 6px;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #888;
}
.wj-foot label {
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
}
.wj-block-btn {
position: absolute;
top: 6px;
right: 6px;
z-index: 9999;
background: rgba(0, 0, 0, 0.68);
color: #fff;
border: none;
border-radius: 5px;
padding: 3px 8px;
font-size: 11px;
line-height: 18px;
cursor: pointer;
display: none;
pointer-events: auto;
}
.wj-block-btn:hover,
.wj-block-btn:focus-visible {
background: #ff3d3d;
outline: none;
}
.wj-hasbtn:hover > .wj-block-btn,
.wj-block-btn:focus-visible {
display: block;
}
.wj-job-hidden {
display: none !important;
}
.wj-delivery {
margin-top: 12px;
padding: 10px;
border: 1px solid #ffe0d8;
border-radius: 9px;
background: #fff8f6;
}
.wj-delivery-actions {
display: flex;
gap: 8px;
align-items: center;
}
.wj-delivery-start {
flex: 1;
background: linear-gradient(135deg, #ff6a3d, #ff3d3d);
}
.wj-delivery-start:hover {
background: linear-gradient(135deg, #ff5730, #ed2929);
}
.wj-delivery-start.wj-delivery-stop {
background: #555;
}
.wj-delivery-start.wj-delivery-stop:hover {
background: #333;
}
.wj-delivery-count {
color: #888;
font-size: 11px;
white-space: nowrap;
}
.wj-delivery-progress {
height: 7px;
margin-top: 9px;
border-radius: 999px;
background: #eadfdb;
overflow: hidden;
}
.wj-delivery-progress-bar {
width: 0;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #ff8b62, #ff3d3d);
transition: width 180ms ease-out;
}
.wj-delivery-status {
min-height: 17px;
margin-top: 7px;
color: #777;
font-size: 11px;
line-height: 17px;
overflow-wrap: anywhere;
}
.wj-delivery-current {
color: #444;
font-weight: 500;
}
.wj-delivery-note {
margin-top: 5px;
color: #aaa;
font-size: 10px;
line-height: 15px;
}
.wj-diagnostic-status {
min-height: 17px;
margin-top: 7px;
color: #777;
font-size: 11px;
line-height: 17px;
overflow-wrap: anywhere;
}
`;
const style = document.createElement('style');
style.textContent = STYLE;
document.head.appendChild(style);
/* =====================================================================
* 构建浮窗
* ===================================================================== */
const deliveryPanelHtml = IS_51JOB || IS_BOSS
? `
等待扫描
仅处理当前页面已加载且未屏蔽的职位。
${IS_BOSS
? '“已触发”表示已点击立即沟通,不代表对方已回复。'
: '“已触发”表示已点击申请按钮,不代表服务端最终投递成功。'}
`
: '';
const bossDiagnosticPanelHtml = IS_BOSS
? `
无需打开 F12;报告不包含 Cookie、Token 或聊天内容。
`
: '';
const zhilianDiagnosticPanelHtml = IS_ZHILIAN
? `
先验证卡片、公司、职位名称和投递按钮结构。
`
: '';
const root = document.createElement('div');
root.id = 'wjBlockerRoot';
root.innerHTML = `
🛡️
0
招聘公司屏蔽器
×
已屏蔽公司
(0)
屏蔽职位关键词
(0)
本次已过滤
(0)
${deliveryPanelHtml}
${bossDiagnosticPanelHtml}
${zhilianDiagnosticPanelHtml}
`;
document.body.appendChild(root);
function getById(id) {
return root.querySelector(`#${id}`);
}
const fab = getById('wjFab');
const panel = getById('wjPanel');
const badge = getById('wjBadge');
const input = getById('wjInput');
const blockedList = getById('wjBlockedList');
const titleInput = getById('wjTitleInput');
const blockedTitleList = getById('wjBlockedTitleList');
const filteredList = getById('wjFilteredList');
const blockedCount = getById('wjBCount');
const blockedTitleCount = getById('wjTCount');
const filteredCount = getById('wjFCount');
const statsElement = getById('wjStats');
const exactCheckbox = getById('wjExact');
const reapplyButton = getById('wjReapply');
const deliveryStartButton = IS_51JOB || IS_BOSS
? getById('wjDeliveryStart')
: null;
const deliveryCountElement = IS_51JOB || IS_BOSS
? getById('wjDeliveryCount')
: null;
const deliveryProgressElement = IS_51JOB || IS_BOSS
? getById('wjDeliveryProgress')
: null;
const deliveryProgressBar = IS_51JOB || IS_BOSS
? getById('wjDeliveryProgressBar')
: null;
const deliveryStatusElement = IS_51JOB || IS_BOSS
? getById('wjDeliveryStatus')
: null;
const bossDiagnosticButton = IS_BOSS
? getById('wjBossDiagnosticStart')
: null;
const bossDiagnosticStatus = IS_BOSS
? getById('wjBossDiagnosticStatus')
: null;
const zhilianDiagnosticButton = IS_ZHILIAN
? getById('wjZhilianDiagnosticStart')
: null;
const zhilianDiagnosticStatus = IS_ZHILIAN
? getById('wjZhilianDiagnosticStatus')
: null;
/* =====================================================================
* Boss 页面诊断(无需开发者工具)
* ===================================================================== */
function getSafeClassName(element) {
return typeof element?.className === 'string'
? element.className
: '';
}
function getSafeElementAttributes(element) {
const allowed = [
'role',
'type',
'title',
'aria-label',
'aria-disabled',
'disabled',
'ka',
];
return Object.fromEntries(
allowed.flatMap((name) => {
const value = element.getAttribute(name);
return value === null
? []
: [[name, value.slice(0, 200)]];
}),
);
}
function buildBossDiagnosticReport() {
const cardSelectors = [
'.job-card-wrapper',
'.job-card-wrap',
'li.job-card',
'[class*="job-card"]',
];
const titleSelectors = [
'.job-name',
'.job-title',
'[class*="job-name"]',
'[class*="job-title"]',
];
const companySelectors = [
'.company-name',
'[class*="company-name"]',
'[class*="company"]',
];
const cardSelector = cardSelectors.join(',');
const titleSelector = titleSelectors.join(',');
const companySelector = companySelectors.join(',');
const cards = Array.from(
new Set(document.querySelectorAll(cardSelector)),
);
const actionPattern =
/沟通|投递|申请|打招呼|感兴趣|联系|chat|apply/i;
const actionCandidates = Array.from(
document.querySelectorAll(
'button, a, [role="button"], [class*="btn"], [class*="button"]',
),
)
.filter((element) => {
if (root.contains(element)) {
return false;
}
const signature = [
normalizeVisibleText(element.textContent),
getSafeClassName(element),
element.getAttribute('title') || '',
element.getAttribute('aria-label') || '',
element.getAttribute('ka') || '',
].join(' ');
return actionPattern.test(signature);
})
.slice(0, 80)
.map((element) => {
const card = element.closest(cardSelector);
return {
tag: element.tagName.toLowerCase(),
text: normalizeVisibleText(element.textContent).slice(0, 100),
className: getSafeClassName(element).slice(0, 300),
attributes: getSafeElementAttributes(element),
visible: element.getClientRects().length > 0,
insideKnownCard: Boolean(card),
cardClassName: getSafeClassName(card).slice(0, 300),
};
});
return {
reportVersion: 1,
lastPopupDiagnostic: lastBossPopupDiagnostic,
generatedAt: new Date().toISOString(),
page: {
origin: location.origin,
pathname: location.pathname,
},
selectorCounts: {
cards: Object.fromEntries(
cardSelectors.map((selector) => [
selector,
document.querySelectorAll(selector).length,
]),
),
titles: Object.fromEntries(
titleSelectors.map((selector) => [
selector,
document.querySelectorAll(selector).length,
]),
),
companies: Object.fromEntries(
companySelectors.map((selector) => [
selector,
document.querySelectorAll(selector).length,
]),
),
},
sampleCards: cards.slice(0, 8).map((card) => ({
tag: card.tagName.toLowerCase(),
className: getSafeClassName(card).slice(0, 300),
title: getElementText(card.querySelector(titleSelector)).slice(0, 100),
company: getElementText(card.querySelector(companySelector)).slice(0, 100),
dataAttributeNames: Array.from(card.attributes)
.map((attribute) => attribute.name)
.filter((name) => name.startsWith('data-')),
linkPath: (() => {
const href = card.querySelector('a[href]')?.getAttribute('href');
if (!href) return null;
try {
return new URL(href, location.href).pathname;
} catch {
return null;
}
})(),
})),
actionCandidates,
};
}
function buildZhilianDiagnosticReport() {
const cardSelectors = [
'.joblist-box__item',
'.joblist-box__iteminfo',
'[class*="joblist-box__item"]',
'[class*="job-card"]',
'li[class*="job"]',
];
const titleSelectors = [
'.job-name',
'[class*="job-name"]',
'[class*="jobname"]',
'a[href*="/jobdetail/"]',
'a[href*="jobs.zhaopin.com"]',
];
const companySelectors = [
'.company-name',
'[class*="company-name"]',
'[class*="companyname"]',
'a[href*="/companydetail/"]',
'a[href*="company"]',
];
const cardSelector = cardSelectors.join(',');
const titleSelector = titleSelectors.join(',');
const companySelector = companySelectors.join(',');
const cards = Array.from(
new Set(document.querySelectorAll(cardSelector)),
);
const actionPattern =
/投递|申请|沟通|打招呼|感兴趣|立即|简历|apply|submit/i;
const actionCandidates = Array.from(
document.querySelectorAll(
'button, a, [role="button"], [class*="btn"], [class*="button"]',
),
)
.filter((element) => {
if (root.contains(element)) {
return false;
}
const signature = [
normalizeVisibleText(element.textContent),
getSafeClassName(element),
element.getAttribute('title') || '',
element.getAttribute('aria-label') || '',
].join(' ');
return actionPattern.test(signature);
})
.slice(0, 100)
.map((element) => {
const card = element.closest(cardSelector);
return {
tag: element.tagName.toLowerCase(),
text: normalizeVisibleText(element.textContent).slice(0, 100),
className: getSafeClassName(element).slice(0, 300),
attributes: getSafeElementAttributes(element),
visible: element.getClientRects().length > 0,
insideKnownCard: Boolean(card),
cardClassName: getSafeClassName(card).slice(0, 300),
};
});
return {
reportVersion: 1,
site: 'zhaopin',
generatedAt: new Date().toISOString(),
page: {
origin: location.origin,
pathname: location.pathname,
},
selectorCounts: {
cards: Object.fromEntries(
cardSelectors.map((selector) => [
selector,
document.querySelectorAll(selector).length,
]),
),
titles: Object.fromEntries(
titleSelectors.map((selector) => [
selector,
document.querySelectorAll(selector).length,
]),
),
companies: Object.fromEntries(
companySelectors.map((selector) => [
selector,
document.querySelectorAll(selector).length,
]),
),
},
sampleCards: cards.slice(0, 10).map((card) => ({
tag: card.tagName.toLowerCase(),
className: getSafeClassName(card).slice(0, 300),
title: getElementText(card.querySelector(titleSelector)).slice(0, 120),
company: getElementText(card.querySelector(companySelector)).slice(0, 120),
dataAttributeNames: Array.from(card.attributes)
.map((attribute) => attribute.name)
.filter((name) => name.startsWith('data-')),
linkPath: (() => {
const href = card.querySelector('a[href]')?.getAttribute('href');
if (!href) return null;
try {
return new URL(href, location.href).pathname;
} catch {
return null;
}
})(),
})),
actionCandidates,
};
}
function downloadDiagnostic(content, prefix) {
const blob = new Blob([content], {
type: 'application/json;charset=utf-8',
});
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `${prefix}-diagnostic-${Date.now()}.json`;
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
async function copyDiagnosticText(content) {
try {
await navigator.clipboard.writeText(content);
return true;
} catch {
const textarea = document.createElement('textarea');
textarea.value = content;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
return document.execCommand('copy');
} catch {
return false;
} finally {
textarea.remove();
}
}
}
async function generateBossDiagnostic() {
if (!IS_BOSS || !bossDiagnosticStatus) {
return;
}
bossDiagnosticStatus.textContent = '正在读取页面结构……';
try {
const report = buildBossDiagnosticReport();
const content = JSON.stringify(report, null, 2);
const copied = await copyDiagnosticText(content);
if (copied) {
bossDiagnosticStatus.textContent =
`诊断已复制:卡片 ${report.sampleCards.length} 个样本,` +
`操作入口 ${report.actionCandidates.length} 个。请直接粘贴发送。`;
} else {
downloadDiagnostic(content, 'boss');
bossDiagnosticStatus.textContent =
'剪贴板不可用,已下载 boss-diagnostic JSON 文件。';
}
} catch (error) {
bossDiagnosticStatus.textContent =
`诊断失败:${error instanceof Error ? error.message : String(error)}`;
}
}
if (bossDiagnosticButton) {
bossDiagnosticButton.addEventListener('click', () => {
void generateBossDiagnostic();
});
}
async function generateZhilianDiagnostic() {
if (!IS_ZHILIAN || !zhilianDiagnosticStatus) {
return;
}
zhilianDiagnosticStatus.textContent = '正在读取智联页面结构……';
try {
const report = buildZhilianDiagnosticReport();
const content = JSON.stringify(report, null, 2);
const copied = await copyDiagnosticText(content);
if (copied) {
zhilianDiagnosticStatus.textContent =
`诊断已复制:卡片 ${report.sampleCards.length} 个样本,` +
`操作入口 ${report.actionCandidates.length} 个。请直接粘贴发送。`;
} else {
downloadDiagnostic(content, 'zhilian');
zhilianDiagnosticStatus.textContent =
'剪贴板不可用,已下载 zhilian-diagnostic JSON 文件。';
}
} catch (error) {
zhilianDiagnosticStatus.textContent =
`诊断失败:${error instanceof Error ? error.message : String(error)}`;
}
}
if (zhilianDiagnosticButton) {
zhilianDiagnosticButton.addEventListener('click', () => {
void generateZhilianDiagnostic();
});
}
/* =====================================================================
* 悬浮球位置及面板
* ===================================================================== */
let position = (() => {
const saved = load(CONFIG.POSITION_KEY, null);
if (
saved &&
typeof saved.x === 'number' &&
typeof saved.y === 'number'
) {
return {
x: saved.x,
y: saved.y,
};
}
return {
x:
window.innerWidth -
CONFIG.FAB_SIZE -
18,
y:
window.innerHeight -
CONFIG.FAB_SIZE -
18,
};
})();
function applyPosition() {
position.x = Math.min(
Math.max(position.x, 0),
Math.max(
window.innerWidth - CONFIG.FAB_SIZE,
0,
),
);
position.y = Math.min(
Math.max(position.y, 0),
Math.max(
window.innerHeight - CONFIG.FAB_SIZE,
0,
),
);
fab.style.left = `${position.x}px`;
fab.style.top = `${position.y}px`;
fab.style.right = 'auto';
fab.style.bottom = 'auto';
}
function positionPanel() {
const panelWidth = Math.min(
CONFIG.PANEL_WIDTH,
window.innerWidth - 16,
);
panel.style.width = `${panelWidth}px`;
const panelHeight = Math.min(
panel.offsetHeight || 480,
window.innerHeight * 0.84,
);
let left =
position.x -
panelWidth +
CONFIG.FAB_SIZE;
let top =
position.y +
CONFIG.FAB_SIZE +
6;
left = Math.min(
Math.max(left, 8),
Math.max(
window.innerWidth - panelWidth - 8,
8,
),
);
if (
top + panelHeight >
window.innerHeight - 8
) {
top =
position.y -
panelHeight -
6;
}
top = Math.min(
Math.max(top, 8),
Math.max(
window.innerHeight - panelHeight - 8,
8,
),
);
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
}
function openPanel() {
panel.classList.add('wj-show');
requestAnimationFrame(() => {
positionPanel();
renderBlocked();
scheduleScan(0);
});
}
function closePanel() {
panel.classList.remove('wj-show');
}
function togglePanel() {
if (panel.classList.contains('wj-show')) {
closePanel();
} else {
openPanel();
}
}
applyPosition();
let dragging = false;
let moved = false;
let startX = 0;
let startY = 0;
let baseX = 0;
let baseY = 0;
fab.addEventListener('pointerdown', (event) => {
if (event.button !== 0) {
return;
}
dragging = true;
moved = false;
startX = event.clientX;
startY = event.clientY;
baseX = position.x;
baseY = position.y;
fab.setPointerCapture(event.pointerId);
});
fab.addEventListener('pointermove', (event) => {
if (!dragging) {
return;
}
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
if (
Math.abs(deltaX) +
Math.abs(deltaY) >
4
) {
moved = true;
}
position.x = baseX + deltaX;
position.y = baseY + deltaY;
applyPosition();
if (panel.classList.contains('wj-show')) {
positionPanel();
}
});
fab.addEventListener('pointerup', (event) => {
if (!dragging) {
return;
}
dragging = false;
if (fab.hasPointerCapture(event.pointerId)) {
fab.releasePointerCapture(event.pointerId);
}
save(CONFIG.POSITION_KEY, {
x: position.x,
y: position.y,
});
if (!moved) {
togglePanel();
}
});
fab.addEventListener('pointercancel', () => {
dragging = false;
});
fab.addEventListener('keydown', (event) => {
if (
event.key === 'Enter' ||
event.key === ' '
) {
event.preventDefault();
togglePanel();
}
});
getById('wjClose').addEventListener(
'click',
closePanel,
);
getById('wjClose').addEventListener(
'keydown',
(event) => {
if (
event.key === 'Enter' ||
event.key === ' '
) {
event.preventDefault();
closePanel();
}
},
);
/* =====================================================================
* 屏蔽名单管理
* ===================================================================== */
function addCompany(value) {
const company = normalizeVisibleText(value);
if (!company) {
return;
}
const normalizedCompany =
normalizeCompanyName(company);
const exists = blocked.some(
(item) =>
normalizeCompanyName(item) ===
normalizedCompany,
);
if (!exists) {
blocked.push(company);
save(CONFIG.STORAGE_KEY, blocked);
}
renderBlocked();
scheduleScan(0);
}
function removeCompany(value) {
const normalizedCompany =
normalizeCompanyName(value);
blocked = blocked.filter(
(item) =>
normalizeCompanyName(item) !==
normalizedCompany,
);
save(CONFIG.STORAGE_KEY, blocked);
renderBlocked();
scheduleScan(0);
}
function addBlockedTitle(value) {
const keyword = normalizeVisibleText(value);
if (!keyword) {
return;
}
const normalizedKeyword = keyword.toLocaleLowerCase();
const exists = blockedTitles.some(
(item) =>
normalizeVisibleText(item).toLocaleLowerCase() ===
normalizedKeyword,
);
if (!exists) {
blockedTitles.push(keyword);
save(CONFIG.TITLE_STORAGE_KEY, blockedTitles);
}
renderBlockedTitles();
scheduleScan(0);
}
function removeBlockedTitle(value) {
const normalizedKeyword = normalizeVisibleText(
value,
).toLocaleLowerCase();
blockedTitles = blockedTitles.filter(
(item) =>
normalizeVisibleText(item).toLocaleLowerCase() !==
normalizedKeyword,
);
save(CONFIG.TITLE_STORAGE_KEY, blockedTitles);
renderBlockedTitles();
scheduleScan(0);
}
function submitCompanyInput() {
const company = input.value;
input.value = '';
addCompany(company);
input.focus();
}
function submitTitleInput() {
const keyword = titleInput.value;
titleInput.value = '';
addBlockedTitle(keyword);
titleInput.focus();
}
getById('wjAdd').addEventListener(
'click',
submitCompanyInput,
);
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
submitCompanyInput();
}
});
getById('wjTitleAdd').addEventListener(
'click',
submitTitleInput,
);
titleInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
submitTitleInput();
}
});
exactCheckbox.addEventListener(
'change',
(event) => {
exactMatch = Boolean(
event.currentTarget.checked,
);
save(CONFIG.EXACT_KEY, exactMatch);
scheduleScan(0);
},
);
getById('wjClearLog').addEventListener(
'click',
() => {
filteredLog.length = 0;
loggedJobs.clear();
renderFiltered();
},
);
reapplyButton.addEventListener(
'click',
() => {
blocked = loadBlockedCompanies();
blockedTitles = loadBlockedTitles();
exactMatch = Boolean(
load(
CONFIG.EXACT_KEY,
CONFIG.DEFAULT_EXACT_MATCH,
),
);
exactCheckbox.checked = exactMatch;
renderBlocked();
renderBlockedTitles();
updateStats('已重新读取并应用筛选配置');
scheduleScan(0);
},
);
function renderBlocked() {
blockedCount.textContent = String(
blocked.length,
);
badge.textContent = String(blocked.length);
if (blocked.length === 0) {
blockedList.innerHTML =
'暂无屏蔽公司,添加后自动过滤其职位。
';
return;
}
blockedList.innerHTML = blocked
.map(
(company) => `
${escapeHtml(company)}
×
`,
)
.join('');
blockedList
.querySelectorAll('.wj-x')
.forEach((removeButton) => {
function remove() {
removeCompany(
removeButton.getAttribute(
'data-company',
),
);
}
removeButton.addEventListener(
'click',
remove,
);
removeButton.addEventListener(
'keydown',
(event) => {
if (
event.key === 'Enter' ||
event.key === ' '
) {
event.preventDefault();
remove();
}
},
);
});
}
/* =====================================================================
* 已过滤记录
* ===================================================================== */
function logFiltered(card, company) {
const identity = [
getJobIdentity(card),
normalizeCompanyName(company),
].join(':');
if (loggedJobs.has(identity)) {
return false;
}
loggedJobs.add(identity);
filteredLog.unshift({
company,
title: extractTitle(card),
time: new Date().toLocaleTimeString(
'zh-CN',
{
hour: '2-digit',
minute: '2-digit',
},
),
});
if (filteredLog.length > CONFIG.MAX_LOG) {
filteredLog.length = CONFIG.MAX_LOG;
}
return true;
}
function renderFiltered() {
filteredCount.textContent = String(
filteredLog.length,
);
if (filteredLog.length === 0) {
filteredList.innerHTML =
'还没有过滤任何职位。
';
return;
}
filteredList.innerHTML = filteredLog
.map(
(entry, index) => `
${escapeHtml(entry.company)}
${escapeHtml(entry.title)}
${escapeHtml(entry.time)}
`,
)
.join('');
filteredList
.querySelectorAll('.wj-unblock')
.forEach((button) => {
button.addEventListener(
'click',
() => {
const index = Number(
button.getAttribute('data-index'),
);
const entry = filteredLog[index];
if (entry) {
removeCompany(entry.company);
}
},
);
});
}
/* =====================================================================
* 职位卡片屏蔽按钮
* ===================================================================== */
function addBlockButton(card) {
const existingButton = Array.from(
card.children,
).find((child) =>
child.classList?.contains('wj-block-btn'),
);
const company = extractCompany(card);
// 页面可能复用卡片 DOM,因此持续更新公司名。
if (existingButton) {
existingButton.dataset.company =
company || '';
return;
}
const button = document.createElement('span');
button.className = 'wj-block-btn';
button.textContent = '屏蔽该公司';
button.dataset.company = company || '';
button.setAttribute('role', 'button');
button.setAttribute('tabindex', '0');
function blockCurrentCompany(event) {
event.stopPropagation();
event.preventDefault();
const currentCompany =
button.dataset.company ||
extractCompany(card);
if (currentCompany) {
addCompany(currentCompany);
return;
}
openPanel();
input.focus();
}
button.addEventListener(
'click',
blockCurrentCompany,
);
button.addEventListener(
'keydown',
(event) => {
if (
event.key === 'Enter' ||
event.key === ' '
) {
blockCurrentCompany(event);
}
},
);
if (
getComputedStyle(card).position ===
'static'
) {
card.style.position = 'relative';
}
card.classList.add('wj-hasbtn');
card.appendChild(button);
}
/* =====================================================================
* 51job 一键投递
* ===================================================================== */
const APPLY_BUTTON_SELECTOR = [
'button.btn.apply',
'button.apply',
'button[class*="apply"]',
'button[track-type="searchTrackButtonClick"][event-type="99"]',
'button[trace-name*="申请"]',
].join(',');
const APPLY_TEXT_PATTERN =
/申请职位|立即申请|投递简历|立即投递|马上申请|申请|投递/u;
const APPLIED_TEXT_PATTERN =
/已申请|已投递|申请成功|投递成功/u;
function isElementVisible(element) {
if (!(element instanceof Element)) {
return false;
}
const computedStyle =
getComputedStyle(element);
if (
computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden' ||
Number(computedStyle.opacity) === 0
) {
return false;
}
return element.getClientRects().length > 0;
}
function getApplyButtons(card) {
const buttons = Array.from(
card.querySelectorAll(APPLY_BUTTON_SELECTOR),
);
if (
card.matches?.(APPLY_BUTTON_SELECTOR) &&
!buttons.includes(card)
) {
buttons.unshift(card);
}
return buttons;
}
function findApplyButton(card) {
const buttons = getApplyButtons(card);
for (const button of buttons) {
const text = normalizeVisibleText(
button.textContent,
);
if (button.disabled) {
continue;
}
if (APPLIED_TEXT_PATTERN.test(text)) {
continue;
}
if (!APPLY_TEXT_PATTERN.test(text)) {
continue;
}
return button;
}
return null;
}
function getDeliveryCards() {
const cards = getCards();
const knownCards = new Set(cards);
// 51job 偶尔会调整列表容器结构。以申请按钮反向定位卡片,
// 避免职位已经显示但通用卡片选择器尚未适配时得到 0 条。
for (const button of document.querySelectorAll(APPLY_BUTTON_SELECTOR)) {
const card = button.closest(
'.joblist-item-job, [sensorsname="JobShortExposure"], li.joblist, .job-card',
);
if (card && !knownCards.has(card)) {
knownCards.add(card);
cards.push(card);
}
}
return cards;
}
function collectDeliverableJobs() {
if (!IS_51JOB) {
return { jobs: [], stats: null };
}
const jobs = [];
const identities = new Set();
const stats = {
cards: 0,
buttons: 0,
hidden: 0,
blocked: 0,
applied: 0,
disabled: 0,
unmatched: 0,
};
for (const card of getDeliveryCards()) {
stats.cards++;
if (
card.classList.contains(
'wj-job-hidden',
) ||
card.dataset.wjHidden === '1'
) {
stats.hidden++;
continue;
}
// 再次检查屏蔽名单,避免页面扫描尚未完成时误投。
if (matchCompany(card) || matchJobTitle(card)) {
stats.blocked++;
continue;
}
const cardButtons = getApplyButtons(card);
stats.buttons += cardButtons.length;
if (cardButtons.length === 0) {
continue;
}
const button = findApplyButton(card);
if (!button) {
for (const candidate of cardButtons) {
const text = normalizeVisibleText(candidate.textContent);
if (APPLIED_TEXT_PATTERN.test(text)) {
stats.applied++;
} else if (
candidate.disabled ||
candidate.getAttribute('aria-disabled') === 'true'
) {
stats.disabled++;
} else {
stats.unmatched++;
}
}
continue;
}
const identity = getJobIdentity(card);
if (identities.has(identity)) {
continue;
}
identities.add(identity);
jobs.push({
identity,
card,
button,
title: extractTitle(card),
company:
extractCompany(card) ||
'(未知公司)',
});
}
return { jobs, stats };
}
function getBossJobId(card) {
const href = card
.querySelector('a[href*="/job_detail/"]')
?.getAttribute('href');
if (!href) {
return '';
}
const match = href.match(/\/job_detail\/([^/?#]+)\.html/i);
return match ? match[1] : '';
}
function collectBossCommunicationJobs() {
if (!IS_BOSS) {
return { jobs: [], stats: null };
}
const jobs = [];
const stats = {
cards: 0,
hidden: 0,
blocked: 0,
missingId: 0,
contacted: 0,
};
const queuedJobIds = new Set();
for (const card of getCards()) {
stats.cards++;
if (
card.classList.contains('wj-job-hidden') ||
card.dataset.wjHidden === '1'
) {
stats.hidden++;
continue;
}
if (matchCompany(card) || matchJobTitle(card)) {
stats.blocked++;
continue;
}
const jobId = getBossJobId(card);
if (!jobId) {
stats.missingId++;
continue;
}
if (
bossContactedJobIds.has(jobId) ||
queuedJobIds.has(jobId)
) {
stats.contacted++;
continue;
}
queuedJobIds.add(jobId);
jobs.push({
identity: `boss:${jobId}`,
jobId,
card,
title: extractTitle(card),
company: extractCompany(card) || '(未知公司)',
});
}
return { jobs, stats };
}
function rememberBossContactedJob(jobId) {
if (!jobId || bossContactedJobIds.has(jobId)) {
return;
}
bossContactedJobIds.add(jobId);
save(CONFIG.BOSS_CONTACTED_KEY, Array.from(bossContactedJobIds));
}
function collectCurrentSiteJobs() {
return IS_BOSS
? collectBossCommunicationJobs()
: collectDeliverableJobs();
}
function getRandomDeliveryDelay() {
const range =
CONFIG.DELIVERY_MAX_DELAY -
CONFIG.DELIVERY_MIN_DELAY +
1;
return (
CONFIG.DELIVERY_MIN_DELAY +
Math.floor(Math.random() * range)
);
}
function waitForDeliveryDelay(delay) {
return new Promise((resolve) => {
const startedAt = Date.now();
function check() {
if (deliveryState.stopRequested) {
resolve();
return;
}
const elapsed = Date.now() - startedAt;
if (elapsed >= delay) {
resolve();
return;
}
setTimeout(
check,
Math.min(100, delay - elapsed),
);
}
check();
});
}
function renderBlockedTitles() {
blockedTitleCount.textContent = String(
blockedTitles.length,
);
if (blockedTitles.length === 0) {
blockedTitleList.innerHTML =
'暂无职位关键词;添加“测试”会隐藏所有名称含“测试”的职位。
';
return;
}
blockedTitleList.innerHTML = blockedTitles
.map(
(keyword) => `
${escapeHtml(keyword)}
×
`,
)
.join('');
blockedTitleList
.querySelectorAll('.wj-x')
.forEach((removeButton) => {
function remove() {
removeBlockedTitle(
removeButton.getAttribute(
'data-title-keyword',
),
);
}
removeButton.addEventListener('click', remove);
removeButton.addEventListener(
'keydown',
(event) => {
if (
event.key === 'Enter' ||
event.key === ' '
) {
event.preventDefault();
remove();
}
},
);
});
}
function updateDeliveryUi() {
if (
(!IS_51JOB && !IS_BOSS) ||
!deliveryStartButton ||
!deliveryCountElement ||
!deliveryProgressElement ||
!deliveryProgressBar ||
!deliveryStatusElement
) {
return;
}
const progress =
deliveryState.total > 0
? Math.min(
100,
Math.round(
(
deliveryState.completed /
deliveryState.total
) * 100,
),
)
: 0;
deliveryProgressBar.style.width =
`${progress}%`;
deliveryProgressElement.setAttribute(
'aria-valuenow',
String(progress),
);
if (deliveryState.running) {
deliveryStartButton.textContent =
deliveryState.stopRequested
? '正在停止...'
: '停止投递';
deliveryStartButton.classList.add(
'wj-delivery-stop',
);
deliveryCountElement.textContent =
`${deliveryState.completed}/${deliveryState.total}`;
} else {
deliveryStartButton.textContent =
IS_BOSS ? '一键沟通当前页' : '一键投递当前页';
deliveryStartButton.classList.remove(
'wj-delivery-stop',
);
if (deliveryState.total > 0) {
deliveryCountElement.textContent =
`${deliveryState.completed}/${deliveryState.total}`;
} else {
const { jobs: availableJobs } =
collectCurrentSiteJobs();
deliveryCountElement.textContent =
`可投 ${availableJobs.length} 条`;
}
}
if (deliveryState.message) {
deliveryStatusElement.textContent =
deliveryState.message;
return;
}
if (
deliveryState.running &&
deliveryState.currentTitle
) {
deliveryStatusElement.innerHTML = [
'正在处理:',
``,
escapeHtml(
`${deliveryState.currentTitle} · ${deliveryState.currentCompany}`,
),
'',
` · 已触发 ${deliveryState.triggered}`,
` · 跳过 ${deliveryState.skipped}`,
].join('');
return;
}
deliveryStatusElement.textContent =
`已触发 ${deliveryState.triggered} 条 · 跳过 ${deliveryState.skipped} 条`;
}
function stopDelivery() {
if (!deliveryState.running) {
return;
}
deliveryState.stopRequested = true;
deliveryState.message =
'正在停止,请稍候……';
updateDeliveryUi();
}
async function runDeliveryQueue(jobs) {
deliveryState.running = true;
deliveryState.stopRequested = false;
deliveryState.total = jobs.length;
deliveryState.completed = 0;
deliveryState.triggered = 0;
deliveryState.skipped = 0;
deliveryState.currentTitle = '';
deliveryState.currentCompany = '';
deliveryState.message = '';
updateDeliveryUi();
try {
for (
let index = 0;
index < jobs.length;
index++
) {
if (deliveryState.stopRequested) {
break;
}
const job = jobs[index];
deliveryState.currentTitle = job.title;
deliveryState.currentCompany =
job.company;
deliveryState.message = '';
updateDeliveryUi();
const currentButton =
job.button.isConnected
? job.button
: job.card.isConnected
? findApplyButton(job.card)
: null;
const shouldSkip =
!job.card.isConnected ||
job.card.classList.contains(
'wj-job-hidden',
) ||
job.card.dataset.wjHidden === '1' ||
Boolean(matchCompany(job.card)) ||
Boolean(matchJobTitle(job.card)) ||
!currentButton ||
!currentButton.isConnected ||
currentButton.disabled ||
currentButton.getAttribute(
'aria-disabled',
) === 'true';
if (shouldSkip) {
deliveryState.skipped++;
deliveryState.completed++;
updateDeliveryUi();
continue;
}
const currentText =
normalizeVisibleText(
currentButton.textContent,
);
if (
APPLIED_TEXT_PATTERN.test(
currentText,
) ||
!APPLY_TEXT_PATTERN.test(currentText)
) {
deliveryState.skipped++;
deliveryState.completed++;
updateDeliveryUi();
continue;
}
try {
currentButton.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
currentButton.click();
deliveryState.triggered++;
} catch {
deliveryState.skipped++;
}
deliveryState.completed++;
updateDeliveryUi();
if (
index < jobs.length - 1 &&
!deliveryState.stopRequested
) {
const delay =
getRandomDeliveryDelay();
deliveryState.message =
`已处理 ${deliveryState.completed}/${deliveryState.total}` +
`,${(delay / 1000).toFixed(1)} 秒后继续……`;
updateDeliveryUi();
await waitForDeliveryDelay(delay);
}
}
} finally {
const stopped =
deliveryState.stopRequested;
deliveryState.running = false;
deliveryState.stopRequested = false;
deliveryState.currentTitle = '';
deliveryState.currentCompany = '';
if (stopped) {
deliveryState.message =
`已停止:完成 ${deliveryState.completed}/${deliveryState.total}` +
`,已触发 ${deliveryState.triggered}` +
`,跳过 ${deliveryState.skipped}。`;
} else {
deliveryState.message =
`处理完成:已触发 ${deliveryState.triggered} 条` +
`,跳过 ${deliveryState.skipped} 条。` +
'请检查页面提示确认实际投递结果。';
}
updateDeliveryUi();
scheduleScan(300);
}
}
function findBossChatButton(jobId) {
const exact = document.querySelector(
`a.op-btn-chat[ka="cpc_job_list_chat_${CSS.escape(jobId)}"]`,
);
if (exact && isElementVisible(exact)) {
return exact;
}
return Array.from(
document.querySelectorAll('a.op-btn-chat, [ka^="cpc_job_list_chat_"]'),
).find((element) =>
isElementVisible(element) &&
/立即沟通/u.test(normalizeVisibleText(element.textContent)),
) || null;
}
function waitForBossChatButton(jobId, timeout = 5000) {
return new Promise((resolve) => {
const startedAt = Date.now();
function check() {
if (deliveryState.stopRequested) {
resolve(null);
return;
}
const button = findBossChatButton(jobId);
if (button) {
resolve(button);
return;
}
if (Date.now() - startedAt >= timeout) {
resolve(null);
return;
}
setTimeout(check, 100);
}
check();
});
}
function findBossStayOnPageAction() {
const textElements = Array.from(
document.querySelectorAll(
'button, a, div, span, [role="button"], [class*="btn"], [class*="button"]',
),
);
for (const textElement of textElements) {
if (
root.contains(textElement) ||
!isElementVisible(textElement) ||
normalizeVisibleText(textElement.textContent) !== '留在此页'
) {
continue;
}
// 只允许真实交互元素,避免误点同时包含“留在此页/继续沟通”的弹窗容器。
const button = textElement.matches(
'button, a, [role="button"]',
)
? textElement
: textElement.closest('button, a, [role="button"]');
if (!button || normalizeVisibleText(button.textContent) !== '留在此页') {
continue;
}
let container = button.parentElement;
let depth = 0;
while (container && container !== document.body && depth < 10) {
const text = normalizeVisibleText(container.textContent);
if (/已向\s*boss\s*发送消息/iu.test(text)) {
return { button, container };
}
container = container.parentElement;
depth++;
}
}
return null;
}
function captureBossPopupDiagnostic(reason) {
const phrasePattern = /已向boss发送消息|留在此页|继续沟通|进入聊天/u;
const visibleElements = Array.from(
document.querySelectorAll(
'div, section, aside, dialog, button, a, [role="dialog"], [role="button"]',
),
).filter((element) => {
if (root.contains(element) || !isElementVisible(element)) {
return false;
}
return phrasePattern.test(
normalizeVisibleText(element.textContent),
);
});
const smallestContainers = visibleElements
.filter((element) =>
!visibleElements.some(
(other) =>
other !== element &&
element.contains(other) &&
phrasePattern.test(
normalizeVisibleText(other.textContent),
),
),
)
.slice(0, 30);
lastBossPopupDiagnostic = {
capturedAt: new Date().toISOString(),
reason,
elements: smallestContainers.map((element) => ({
tag: element.tagName.toLowerCase(),
text: normalizeVisibleText(element.textContent).slice(0, 500),
className: getSafeClassName(element).slice(0, 500),
attributes: getSafeElementAttributes(element),
parentTag: element.parentElement?.tagName.toLowerCase() || null,
parentClassName: getSafeClassName(element.parentElement).slice(0, 500),
parentText: normalizeVisibleText(
element.parentElement?.textContent,
).slice(0, 800),
})),
};
}
function waitForBossSentDialog(timeout = 5000) {
return new Promise((resolve) => {
const startedAt = Date.now();
function check() {
if (deliveryState.stopRequested) {
resolve(null);
return;
}
const action = findBossStayOnPageAction();
if (action) {
resolve(action);
return;
}
if (Date.now() - startedAt >= timeout) {
resolve(null);
return;
}
setTimeout(check, 100);
}
check();
});
}
function waitForBossSentDialogToClose(action, timeout = 4000) {
return new Promise((resolve) => {
const startedAt = Date.now();
function check() {
const dialogClosed =
!action.container.isConnected ||
!isElementVisible(action.container) ||
!normalizeVisibleText(action.container.textContent)
.includes('已向boss发送消息');
if (dialogClosed) {
resolve(true);
return;
}
if (Date.now() - startedAt >= timeout) {
resolve(false);
return;
}
setTimeout(check, 100);
}
check();
});
}
async function closeBossSentDialog() {
deliveryState.message = '等待 Boss 发送结果弹窗……';
updateDeliveryUi();
const action = await waitForBossSentDialog();
if (!action) {
captureBossPopupDiagnostic('未找到“留在此页”按钮或匹配弹窗');
return false;
}
deliveryState.message = '已发送消息,正在点击“留在此页”……';
updateDeliveryUi();
try {
action.button.click();
} catch {
captureBossPopupDiagnostic('找到按钮,但调用 click() 失败');
return false;
}
deliveryState.message = '正在等待弹窗关闭……';
updateDeliveryUi();
const closed = await waitForBossSentDialogToClose(action);
if (!closed) {
captureBossPopupDiagnostic('点击后未确认弹窗关闭');
}
return closed;
}
async function runBossCommunicationQueue(jobs) {
deliveryState.running = true;
deliveryState.stopRequested = false;
deliveryState.total = jobs.length;
deliveryState.completed = 0;
deliveryState.triggered = 0;
deliveryState.skipped = 0;
deliveryState.currentTitle = '';
deliveryState.currentCompany = '';
deliveryState.message = '';
updateDeliveryUi();
try {
for (let index = 0; index < jobs.length; index++) {
if (deliveryState.stopRequested) {
break;
}
const job = jobs[index];
deliveryState.currentTitle = job.title;
deliveryState.currentCompany = job.company;
deliveryState.message = '正在切换右侧职位详情……';
updateDeliveryUi();
if (
!job.card.isConnected ||
job.card.classList.contains('wj-job-hidden') ||
matchCompany(job.card) ||
matchJobTitle(job.card)
) {
deliveryState.skipped++;
deliveryState.completed++;
updateDeliveryUi();
continue;
}
job.card.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
job.card.click();
const chatButton = await waitForBossChatButton(job.jobId);
if (
!chatButton ||
normalizeVisibleText(chatButton.textContent) !== '立即沟通'
) {
deliveryState.skipped++;
} else {
try {
chatButton.click();
deliveryState.triggered++;
// Boss 沟通后按钮仍显示“立即沟通”,必须按职位 ID 自行去重。
rememberBossContactedJob(job.jobId);
const dialogClosed = await closeBossSentDialog();
if (!dialogClosed) {
deliveryState.message =
'未能确认“留在此页”弹窗已关闭,已停止队列以避免误操作。';
deliveryState.stopRequested = true;
}
} catch {
deliveryState.skipped++;
}
}
deliveryState.completed++;
updateDeliveryUi();
if (
index < jobs.length - 1 &&
!deliveryState.stopRequested
) {
const delay = getRandomDeliveryDelay();
deliveryState.message =
`已处理 ${deliveryState.completed}/${deliveryState.total}` +
`,${(delay / 1000).toFixed(1)} 秒后继续……`;
updateDeliveryUi();
await waitForDeliveryDelay(delay);
}
}
} finally {
const stopped = deliveryState.stopRequested;
const stopMessage = deliveryState.message;
deliveryState.running = false;
deliveryState.stopRequested = false;
deliveryState.currentTitle = '';
deliveryState.currentCompany = '';
deliveryState.message = stopped && stopMessage.includes('未能确认')
? `${stopMessage} 当前进度 ${deliveryState.completed}/${deliveryState.total}。` +
'请手动关闭弹窗,然后点击“生成 Boss 诊断”。'
: stopped
? `已停止:完成 ${deliveryState.completed}/${deliveryState.total},` +
`已触发 ${deliveryState.triggered},跳过 ${deliveryState.skipped}。`
: `处理完成:已触发沟通 ${deliveryState.triggered} 条,` +
`跳过 ${deliveryState.skipped} 条。`;
updateDeliveryUi();
scheduleScan(300);
}
}
async function startBossCommunication() {
scanAll();
const { jobs, stats } = collectBossCommunicationJobs();
if (jobs.length === 0) {
deliveryState.total = 0;
deliveryState.completed = 0;
deliveryState.triggered = 0;
deliveryState.skipped = 0;
deliveryState.message = stats
? `没有可沟通职位:检测卡片 ${stats.cards},` +
`已屏蔽 ${stats.blocked + stats.hidden},` +
`已记录沟通 ${stats.contacted},缺少职位 ID ${stats.missingId}。`
: '当前页面没有可沟通职位。';
updateDeliveryUi();
return;
}
const confirmed = window.confirm(
`即将依次处理当前页面 ${jobs.length} 个 Boss 职位。\n\n` +
'脚本会逐条选择职位,在右侧详情点击“立即沟通”。\n' +
'已屏蔽的公司和职位关键词会跳过。\n' +
'每次随机间隔 0.8~2.0 秒,运行中可停止。\n\n' +
'是否开始?',
);
if (confirmed) {
await runBossCommunicationQueue(jobs);
}
}
async function startDelivery() {
if (deliveryState.running) {
stopDelivery();
return;
}
if (IS_BOSS) {
await startBossCommunication();
return;
}
if (!IS_51JOB) {
return;
}
// 开始前同步扫描一次,确保屏蔽名单已应用。
scanAll();
const { jobs, stats } = collectDeliverableJobs();
if (jobs.length === 0) {
deliveryState.total = 0;
deliveryState.completed = 0;
deliveryState.triggered = 0;
deliveryState.skipped = 0;
const details = stats
? `检测卡片 ${stats.cards},申请按钮 ${stats.buttons},` +
`已申请 ${stats.applied},禁用 ${stats.disabled},` +
`已屏蔽 ${stats.blocked + stats.hidden},未识别 ${stats.unmatched}。`
: '';
deliveryState.message =
`当前页面没有可投递职位。${details}`;
updateDeliveryUi();
return;
}
const confirmed = window.confirm(
`即将依次触发当前页面 ${jobs.length} 个职位的申请按钮。\n\n` +
'执行规则:\n' +
'• 跳过屏蔽公司\n' +
'• 跳过已申请职位\n' +
'• 跳过不可见或按钮不可用的职位\n' +
'• 每次随机间隔 0.8~2.0 秒\n' +
'• 运行过程中可点击“停止投递”\n' +
'• 登录、确认弹窗或验证码需要手动处理\n\n' +
'“已触发”只表示脚本点击了申请按钮,不代表服务端最终投递成功。\n\n' +
'是否开始?',
);
if (!confirmed) {
return;
}
await runDeliveryQueue(jobs);
}
if (deliveryStartButton) {
deliveryStartButton.addEventListener(
'click',
() => {
void startDelivery();
},
);
}
/* =====================================================================
* 扫描及过滤
* ===================================================================== */
function updateStats(message) {
if (!statsElement) {
return;
}
statsElement.textContent =
message ||
`本页检测到 ${statDetected} 条职位 · 当前隐藏 ${statHidden} 条`;
}
function scheduleScan(
delay = CONFIG.SCAN_DELAY,
) {
clearTimeout(scanTimer);
scanTimer = setTimeout(
scanAll,
delay,
);
}
function scanAll() {
if (scanning) {
scanQueued = true;
return;
}
scanning = true;
scanQueued = false;
try {
const cards = getCards();
let logChanged = false;
statDetected = cards.length;
statHidden = 0;
for (const card of cards) {
addBlockButton(card);
const matchedCompany = matchCompany(card);
const matchedTitle = matchJobTitle(card);
const matchedReason = matchedCompany
? matchedCompany
: matchedTitle
? `职位关键词:${matchedTitle}`
: null;
const wasHidden =
card.classList.contains(
'wj-job-hidden',
);
if (matchedReason) {
statHidden++;
card.classList.add(
'wj-job-hidden',
);
card.dataset.wjHidden = '1';
if (
!wasHidden &&
logFiltered(
card,
matchedReason,
)
) {
logChanged = true;
}
} else {
card.classList.remove(
'wj-job-hidden',
);
delete card.dataset.wjHidden;
}
}
if (logChanged) {
renderFiltered();
}
updateStats();
if (
(IS_51JOB || IS_BOSS) &&
!deliveryState.running
) {
updateDeliveryUi();
}
} finally {
scanning = false;
if (scanQueued) {
scheduleScan(50);
}
}
}
/* =====================================================================
* 动态页面监听
* ===================================================================== */
function isScriptOwnedElement(node) {
if (!(node instanceof Element)) {
return false;
}
return (
node === root ||
root.contains(node) ||
node.classList.contains(
'wj-block-btn',
)
);
}
function isRelevantMutation(record) {
if (root.contains(record.target)) {
return false;
}
if (record.type === 'characterData') {
return true;
}
const changedNodes = [
...record.addedNodes,
...record.removedNodes,
];
if (changedNodes.length === 0) {
return true;
}
return changedNodes.some((node) => {
if (node.nodeType === Node.TEXT_NODE) {
return true;
}
return !isScriptOwnedElement(node);
});
}
const observer = new MutationObserver(
(records) => {
if (records.some(isRelevantMutation)) {
scheduleScan();
}
},
);
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
});
/* =====================================================================
* 初始化
* ===================================================================== */
function init() {
renderBlocked();
renderBlockedTitles();
renderFiltered();
scanAll();
if (IS_51JOB || IS_BOSS) {
updateDeliveryUi();
}
}
init();
window.addEventListener(
'load',
() => {
scheduleScan(0);
},
{ once: true },
);
window.addEventListener(
'pageshow',
() => {
scheduleScan(0);
},
);
document.addEventListener(
'visibilitychange',
() => {
if (!document.hidden) {
scheduleScan(0);
}
},
);
window.addEventListener(
'resize',
() => {
applyPosition();
if (panel.classList.contains('wj-show')) {
positionPanel();
}
},
{ passive: true },
);
})();