// ==UserScript==
// @name 禾急 - BOSS海投助手
// @namespace heji-boss-helper
// @version 1.0.0
// @description BOSS直聘AI海投助手:一键批量投递 + AI智能回复 + 自动发简历 + 数据看板
// @author 禾急
// @match *://www.zhipin.com/web/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_notification
// @connect api.deepseek.com
// @connect api.openai.com
// @connect api.siliconflow.cn
// @connect localhost
// @connect 127.0.0.1
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
// ═══════════════════════════════════════════════════════════════
// CONFIG
// ═══════════════════════════════════════════════════════════════
const C = {
VERSION: "1.0.0",
STORAGE_PREFIX: "heji_boss_",
DELAYS: { CLICK: 800, SCROLL: 1200, CHAT_CHECK: 3000, AI_TIMEOUT: 30000 },
LIMITS: { MAX_RECORDS: 1000, MAX_HR_TRACK: 500, MAX_GREETINGS: 20 },
PANEL_WIDTH: 360,
};
// ═══════════════════════════════════════════════════════════════
// STORAGE
// ═══════════════════════════════════════════════════════════════
const S = {
get(k, fallback) {
try { const v = localStorage.getItem(C.STORAGE_PREFIX + k); return v !== null ? JSON.parse(v) : fallback; }
catch { return fallback; }
},
set(k, v) {
try { localStorage.setItem(C.STORAGE_PREFIX + k, JSON.stringify(v)); } catch {}
},
remove(k) { localStorage.removeItem(C.STORAGE_PREFIX + k); },
};
// ═══════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════
const state = {
isRunning: false,
currentJobIndex: 0,
jobList: [],
processedHRs: new Set(),
sentGreetings: new Set(),
sentResumes: new Set(),
totalApplied: S.get("totalApplied", 0),
todayApplied: S.get("todayApplied", 0),
records: S.get("records", []),
init() {
const today = new Date().toDateString();
const lastDate = S.get("lastDate", "");
if (today !== lastDate) { S.set("todayApplied", 0); S.set("lastDate", today); this.todayApplied = 0; }
}
};
state.init();
// ═══════════════════════════════════════════════════════════════
// DOM UTILS
// ═══════════════════════════════════════════════════════════════
const $ = (sel, parent = document) => parent.querySelector(sel);
const $$ = (sel, parent = document) => [...parent.querySelectorAll(sel)];
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function waitFor(sel, timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = $(sel); if (el) return el;
await sleep(200);
}
return null;
}
function simulateClick(el) {
if (!el) return;
["mouseover", "mousedown", "mouseup", "click"].forEach(t =>
el.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true }))
);
}
// ═══════════════════════════════════════════════════════════════
// AI CLIENT
// ═══════════════════════════════════════════════════════════════
const AI = {
getConfig() {
return {
key: S.get("aiKey", "sk-fd50e093b07b44098f47be79082d6178"),
url: S.get("aiUrl", "https://api.deepseek.com/v1/chat/completions"),
model: S.get("aiModel", "deepseek-chat"),
role: S.get("aiRole", "你是求职者的AI助手,帮求职者与HR高效沟通。回复简洁、礼貌、专业,控制在50字以内。"),
};
},
async chat(messages) {
const cfg = this.getConfig();
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST", url: cfg.url,
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + cfg.key },
data: JSON.stringify({ model: cfg.model, messages, max_tokens: 300, temperature: 0.7 }),
timeout: C.DELAYS.AI_TIMEOUT,
onload(r) {
try {
const d = JSON.parse(r.responseText);
resolve(d.choices?.[0]?.message?.content || d.choices?.[0]?.text || "");
} catch { reject(new Error("AI response parse failed")); }
},
onerror: reject,
});
});
},
async reply(hrMessage, context = "") {
const cfg = this.getConfig();
const msgs = [
{ role: "system", content: cfg.role },
{ role: "user", content: `HR说:"${hrMessage}"\n${context}\n请生成一个简洁得体的回复(50字以内):` }
];
return this.chat(msgs);
},
};
// ═══════════════════════════════════════════════════════════════
// UI - Control Panel
// ═══════════════════════════════════════════════════════════════
const UI = {
panel: null, mini: null, minimized: false,
css: `
#heji-panel{position:fixed;right:16px;top:60px;width:${C.PANEL_WIDTH}px;max-height:80vh;
background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,0.12);
z-index:2147483647;overflow:hidden;font-size:13px;color:#333;font-family:system-ui,sans-serif;}
#heji-panel.minimized{width:48px;height:48px;border-radius:50%;overflow:hidden;}
#heji-panel.minimized .heji-body{display:none;}
#heji-header{background:#1a1a1a;color:#fff;padding:10px 14px;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;}
#heji-header h3{margin:0;font-size:14px;font-weight:700;letter-spacing:0.5px;}
#heji-header .heji-btns{display:flex;gap:6px;}
#heji-header button{background:none;border:none;color:#aaa;cursor:pointer;font-size:16px;padding:0 4px;line-height:1;}
#heji-header button:hover{color:#fff;}
.heji-body{padding:12px 14px;overflow-y:auto;max-height:calc(80vh - 42px);}
.heji-section{margin-bottom:14px;}
.heji-section label{display:block;font-size:11px;font-weight:600;color:#888;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.5px;}
.heji-section input,.heji-section select{width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:12px;outline:none;box-sizing:border-box;}
.heji-section input:focus,.heji-section select:focus{border-color:#2563eb;}
.heji-btn{display:block;width:100%;padding:10px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;text-align:center;transition:all 0.15s;}
.heji-btn.primary{background:#2563eb;color:#fff;}
.heji-btn.primary:hover{background:#1d4ed8;}
.heji-btn.primary.running{background:#dc2626;animation:pulse 1.5s infinite;}
.heji-btn.secondary{background:#f3f4f6;color:#333;margin-top:6px;}
.heji-btn.secondary:hover{background:#e5e7eb;}
.heji-btn:disabled{opacity:0.5;cursor:not-allowed;}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}
.heji-stats{display:flex;gap:8px;text-align:center;}
.heji-stat{flex:1;background:#f8fafc;border-radius:8px;padding:8px 6px;}
.heji-stat .num{font-size:20px;font-weight:800;color:#2563eb;}
.heji-stat .lbl{font-size:10px;color:#888;margin-top:2px;}
.heji-tag{display:inline-block;font-size:10px;padding:2px 6px;border-radius:4px;margin:2px;}
.heji-tag.green{background:#dcfce7;color:#166534;}
.heji-tag.red{background:#fef2f2;color:#991b1b;}
.heji-tag.blue{background:#dbeafe;color:#1e40af;}
.heji-log{font-size:11px;color:#666;max-height:120px;overflow-y:auto;background:#fafafa;border-radius:6px;padding:8px;}
.heji-log p{margin:0 0 4px 0;border-bottom:1px solid #f0f0f0;padding-bottom:4px;}
#heji-mini{position:fixed;left:16px;bottom:80px;width:48px;height:48px;border-radius:50%;background:#2563eb;color:#fff;display:none;align-items:center;justify-content:center;cursor:pointer;z-index:2147483647;font-size:20px;font-weight:900;box-shadow:0 4px 12px rgba(37,99,235,0.4);}
`,
init() {
GM_addStyle(this.css);
this.build();
this.bindEvents();
},
build() {
const isJobs = location.pathname.includes("/jobs");
const p = document.createElement("div"); p.id = "heji-panel";
p.innerHTML = `
${state.totalApplied}
累计投递
${state.todayApplied}
今日投递
${S.get("totalReplies",0)}
收到回复
${isJobs ? `
` : `
`}
${!isJobs ? `
` : ""}
`;
const mini = document.createElement("div"); mini.id = "heji-mini"; mini.textContent = "⚡";
document.body.appendChild(p); document.body.appendChild(mini);
this.panel = p; this.mini = mini;
},
bindEvents() {
const startBtn = $("#heji-start") || $("#heji-chat-start");
if (startBtn) startBtn.addEventListener("click", () => {
if (state.isRunning) { Engine.stop(); } else { Engine.start(); }
});
$("#heji-min")?.addEventListener("click", () => this.toggleMinimize());
this.mini?.addEventListener("click", () => this.toggleMinimize());
$("#heji-settings")?.addEventListener("click", () => Dialog.settings());
$("#heji-dash")?.addEventListener("click", () => Dash.show());
$("#heji-settings-btn")?.addEventListener("click", () => Dialog.settings());
const header = $("#heji-header"); let dx=0, dy=0, dragging=false;
header?.addEventListener("mousedown", e => { dragging=true; dx=e.clientX-this.panel.offsetLeft; dy=e.clientY-this.panel.offsetTop; });
document.addEventListener("mousemove", e => { if(dragging){ this.panel.style.left=(e.clientX-dx)+"px"; this.panel.style.top=(e.clientY-dy)+"px"; this.panel.style.right="auto"; } });
document.addEventListener("mouseup", () => { dragging=false; });
},
toggleMinimize() {
this.minimized = !this.minimized;
this.panel.classList.toggle("minimized", this.minimized);
this.mini.style.display = this.minimized ? "flex" : "none";
},
log(msg) { const el = $("#heji-log"); if (el) { el.innerHTML = `${new Date().toLocaleTimeString()} ${msg}
` + el.innerHTML; if (el.children.length > 50) el.removeChild(el.lastChild); } },
updateStats() {
const total = $("#heji-total"), today = $("#heji-today"), reply = $("#heji-reply");
if (total) total.textContent = state.totalApplied;
if (today) today.textContent = state.todayApplied;
if (reply) reply.textContent = S.get("totalReplies", 0);
},
setRunning(run) {
const btn = $("#heji-start") || $("#heji-chat-start");
if (btn) { btn.textContent = run ? "⏹ 停止" : (location.pathname.includes("/jobs") ? "🚀 启动海投" : "💬 开始智能聊天"); btn.classList.toggle("running", run); }
},
};
// ═══════════════════════════════════════════════════════════════
// SETTINGS DIALOG
// ═══════════════════════════════════════════════════════════════
const Dialog = {
settings() {
if ($("#heji-overlay")) return;
const overlay = document.createElement("div"); overlay.id = "heji-overlay";
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:2147483648;display:flex;align-items:center;justify-content:center;";
const cfg = AI.getConfig();
const greetings = S.get("greetings", ["您好,我对这个岗位很感兴趣,方便聊聊吗?", "您好,看到贵司的招聘信息,我的经验和技能比较匹配,期待进一步沟通。"]);
overlay.innerHTML = ``;
document.body.appendChild(overlay);
$("#dlg-cancel").onclick = () => overlay.remove();
$("#dlg-save").onclick = () => {
S.set("aiKey", $("#dlg-key").value);
S.set("aiUrl", $("#dlg-url").value);
S.set("aiModel", $("#dlg-model").value);
S.set("aiRole", $("#dlg-role").value);
S.set("greetings", $("#dlg-greetings").value.split("\n").filter(l => l.trim()));
S.set("selfIntro", $("#dlg-self-intro").value);
overlay.remove();
UI.log("\u2705 设置已保存");
};
$("#dlg-gen-greetings").onclick = async () => {
const btn = $("#dlg-gen-greetings");
btn.textContent = "\u23f3 AI\u5206\u6790\u751f\u6210\u4e2d..."; btn.disabled = true;
$("#dlg-gen-preview").innerHTML = "";
try {
const selfIntro = $("#dlg-self-intro").value.trim();
const targetJob = $("#dlg-target-job").value.trim();
if (!selfIntro) { $("#dlg-gen-preview").textContent = "\u26a0 \u8bf7\u5148\u586b\u5199\u81ea\u6211\u8bc4\u4ef7"; btn.disabled = false; return; }
const sysPrompt = [
"你是资深求职顾问,专精于帮求职者写出高回复率的BOSS直聘打招呼语。",
"",
"## 核心原则",
"1. 从自我评价提取并提炼核心信息,不是简单复制原文",
"2. 只引用与目标岗位最匹配的1-2项核心经历,不罗列全部",
"3. 避免信息过载,每条50~120字",
"",
"## 严禁的套路话术",
"- \"希望贵公司给我一个机会\"",
"- \"我是一个认真负责的人\"",
"- \"看到贵司招聘信息非常感兴趣\"",
"- \"期待您的回复\"",
"",
"## 输出格式:生成3个版本,严格按以下格式",
"【A版】自然亲和:真实沟通感,像职场同事聊天",
"【B版】专业价值:突出匹配度和量化成果",
"【C版】高回复率:用具体数据/项目成果吸引HR点简历",
].join("\n");
const userMsg = targetJob
? "目标岗位:" + targetJob + "\n我的自我评价:\n" + selfIntro + "\n\n生成3个版本,每个50-120字。"
: "我的自我评价:\n" + selfIntro + "\n\n生成3个版本,每个50-120字。";
const reply = await AI.chat([
{ role: "system", content: sysPrompt },
{ role: "user", content: userMsg }
]);
if (reply) {
const aM = reply.match(/【A版】\s*([\s\S]*?)(?=【B版】|$)/);
const bM = reply.match(/【B版】\s*([\s\S]*?)(?=【C版】|$)/);
const cM = reply.match(/【C版】\s*([\s\S]*?)$/);
const results = [];
if (aM) results.push(aM[1].trim());
if (bM) results.push(bM[1].trim());
if (cM) results.push(cM[1].trim());
if (results.length > 0) {
$("#dlg-greetings").value = results.join("\n");
const labels = ["A(亲和)", "B(专业)", "C(高回复)"];
let preview = "";
results.forEach((r, i) => preview += "" + labels[i] + ": " + r + "
");
$("#dlg-gen-preview").innerHTML = preview;
} else {
const lines = reply.split("\n").filter(l => l.trim().length > 10);
$("#dlg-greetings").value = lines.join("\n");
$("#dlg-gen-preview").textContent = "\u2705 已生成 " + lines.length + " 条";
}
}
} catch (e) {
$("#dlg-gen-preview").textContent = "\u274c 失败: " + e.message;
}
btn.textContent = "\u{1F916} AI生成高质量打招呼语(3版本)"; btn.disabled = false;
};
overlay.addEventListener("click", e => { if (e.target === overlay) overlay.remove(); });
},
};
// ═══════════════════════════════════════════════════════════════
// DASHBOARD
// ═══════════════════════════════════════════════════════════════
const Dash = {
show() {
const records = S.get("records", []);
if ($("#heji-dash-overlay")) { $("#heji-dash-overlay").remove(); return; }
const overlay = document.createElement("div"); overlay.id = "heji-dash-overlay";
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:2147483648;display:flex;align-items:center;justify-content:center;";
const today = new Date().toDateString();
const todayRecords = records.filter(r => r.date === today);
overlay.innerHTML = `
📊 投递看板
${todayRecords.length}
今日投递
${S.get("totalReplies",0)}
收到回复
| 公司 | 岗位 | 薪资 | 状态 | 时间 |
${records.slice(-50).reverse().map(r => `| ${r.company||"-"} | ${r.title||"-"} | ${r.salary||"-"} | 已投递 | ${r.time||""} |
`).join("")}
`;
document.body.appendChild(overlay);
$("#heji-dash-close").onclick = () => overlay.remove();
overlay.addEventListener("click", e => { if (e.target === overlay) overlay.remove(); });
},
};
// ═══════════════════════════════════════════════════════════════
// FILTER SYNC - Click BOSS直聘 page filters
// ═══════════════════════════════════════════════════════════════
const FilterSync = {
// Map our values to BOSS filter button text
async sync(exp, edu, salary) {
if (!$("#heji-sync-filters")?.checked) return;
UI.log("🔧 同步BOSS筛选条件...");
// Click filter dropdowns one by one
const filterBar = $(".search-condition");
if (!filterBar) return;
const filterBtns = $$(".condition-container .condition-title", filterBar);
// Experience filter - usually the 3rd or 4th filter button
if (exp) await this.clickFilterOption(filterBtns, exp);
if (edu) await this.clickFilterOption(filterBtns, edu);
if (salary) await this.clickFilterOption(filterBtns, salary);
},
async clickFilterOption(btns, targetText) {
for (const btn of btns) {
const text = btn.textContent?.trim() || "";
// Click the dropdown to open it
simulateClick(btn);
await sleep(500);
// Find the option in the dropdown
const options = $$(".condition-list-item span, .dropdown-item span, .filter-option");
for (const opt of options) {
if (opt.textContent?.trim() === targetText) {
simulateClick(opt);
await sleep(300);
return true;
}
}
// Close dropdown if not matched
simulateClick(btn);
await sleep(200);
}
return false;
},
};
// ═══════════════════════════════════════════════════════════════
// MAIN ENGINE
// ═══════════════════════════════════════════════════════════════
const Engine = {
async start() {
if (state.isRunning) return;
state.isRunning = true;
UI.setRunning(true);
UI.log("🚀 启动中...");
const keywords = $("#heji-keywords")?.value || "";
const location = $("#heji-location")?.value || "";
const exp = $("#heji-exp")?.value || "";
const edu = $("#heji-edu")?.value || "";
const salary = $("#heji-salary")?.value || "";
S.set("keywords", keywords); S.set("location", location);
S.set("exp", exp); S.set("edu", edu); S.set("salary", salary);
if (location.pathname.includes("/jobs")) {
// Sync filters to BOSS page first
await FilterSync.sync(exp, edu, salary);
await sleep(2000); // Wait for page to reload filtered results
await this.scanAndApply(keywords, location);
} else if (location.pathname.includes("/chat")) {
await this.chatLoop();
}
},
stop() {
state.isRunning = false;
UI.setRunning(false);
UI.log("⏹ 已停止");
},
async scanAndApply(keywords, location) {
const kwList = keywords.split(/[,,]/).map(s => s.trim()).filter(Boolean);
const locList = location.split(/[,,]/).map(s => s.trim()).filter(Boolean);
const expVal = $("#heji-exp")?.value || "";
const eduVal = $("#heji-edu")?.value || "";
const salaryVal = $("#heji-salary")?.value || "";
UI.log("📜 加载职位列表...");
for (let i = 0; i < 10 && state.isRunning; i++) {
window.scrollTo(0, document.body.scrollHeight);
await sleep(C.DELAYS.SCROLL);
}
const cards = $$("li.job-card-box");
UI.log(`📋 找到 ${cards.length} 个职位`);
let count = 0;
for (const card of cards) {
if (!state.isRunning) break;
const title = $(".job-name", card)?.textContent?.trim() || "";
const salary = $(".salary", card)?.textContent?.trim() || "";
const loc = $(".job-area", card)?.textContent?.trim() || "";
const company = $(".company-name", card)?.textContent?.trim() || "";
// Extract experience and education from tags or job-info
const allTags = $$(".tag-item, .job-exp, .experience, .job-info-item", card).map(e => e.textContent?.trim() || "").join(" ");
const cardText = title + " " + salary + " " + loc + " " + allTags;
if (kwList.length && !kwList.some(k => cardText.includes(k))) continue;
if (locList.length && !locList.some(l => loc.includes(l))) continue;
if (expVal && !allTags.includes(expVal)) continue;
if (eduVal && !allTags.includes(eduVal)) continue;
if (salaryVal) {
// Parse salary range for comparison
const cardMax = this.parseSalaryMax(salary);
const filterMax = this.parseSalaryMax(salaryVal.replace("以上",""));
if (filterMax > 0 && cardMax > 0 && cardMax < filterMax) continue;
}
card.scrollIntoView({ behavior: "smooth", block: "center" });
await sleep(C.DELAYS.CLICK);
const chatBtn = [...$$("a.op-btn-chat", card)].find(b => b.textContent.includes("立即沟通"));
if (!chatBtn) continue;
simulateClick(chatBtn);
await sleep(C.DELAYS.CLICK);
const stayBtn = await waitFor(".default-btn.cancel-btn", 2000);
if (stayBtn) { simulateClick(stayBtn); await sleep(500); }
count++;
state.totalApplied++;
state.todayApplied++;
state.records.push({ title, company, salary, location: loc, status: "已投递", date: new Date().toDateString(), time: new Date().toLocaleTimeString() });
if (state.records.length > C.LIMITS.MAX_RECORDS) state.records.shift();
S.set("totalApplied", state.totalApplied);
S.set("todayApplied", state.todayApplied);
S.set("records", state.records);
UI.updateStats();
UI.log(`✅ ${title} @ ${company} ${salary}`);
const greetings = S.get("greetings", ["您好,我对这个岗位很感兴趣,方便聊聊吗?"]);
const msg = greetings[Math.floor(Math.random() * greetings.length)];
const input = await waitFor("#chat-input", 3000);
if (input) {
input.value = msg;
input.dispatchEvent(new Event("input", { bubbles: true }));
await sleep(300);
const sendBtn = $(".btn-send");
if (sendBtn) simulateClick(sendBtn);
UI.log(`💬 已发送招呼语`);
}
await sleep(C.DELAYS.CLICK);
}
S.set("records", state.records);
state.isRunning = false;
UI.setRunning(false);
UI.log(`✅ 海投完成!共投递 ${count} 个职位`);
},
// Parse salary max from BOSS format like "15K-25K" or "20K-30K·14薪"
parseSalaryMax(s) {
const match = s.match(/(\d+)[kK]/g);
if (match && match.length >= 2) return parseInt(match[1]);
if (match) return parseInt(match[0]);
return 0;
},
async chatLoop() {
UI.log("💬 开始监控聊天...");
while (state.isRunning) {
const chatItems = $$('ul[role="group"] li[role="listitem"]');
for (const item of chatItems) {
if (!state.isRunning) break;
const name = $(".name", item)?.textContent?.trim() || "";
const lastMsg = $(".last-msg", item)?.textContent?.trim() || "";
const key = name + lastMsg;
if (state.processedHRs.has(key)) continue;
state.processedHRs.add(key);
simulateClick(item);
await sleep(1000);
const hrMsg = $("li.message-item.item-friend:last-child .text span")?.textContent?.trim();
if (hrMsg) {
UI.log(`📩 ${name}: ${hrMsg.substring(0,30)}...`);
try {
const reply = await AI.reply(hrMsg, "");
const input = await waitFor("#chat-input", 2000);
if (input && reply) {
input.value = reply;
input.dispatchEvent(new Event("input", { bubbles: true }));
await sleep(500);
const sendBtn = $(".btn-send");
if (sendBtn) simulateClick(sendBtn);
state.replyCount = (state.replyCount || 0) + 1;
S.set("totalReplies", state.replyCount);
UI.updateStats();
UI.log(`🤖 已回复: ${reply.substring(0,30)}...`);
}
} catch (e) { UI.log(`❌ AI回复失败: ${e.message}`); }
if (S.get("autoResume", true)) {
await sleep(1000);
const resumeBtn = [...$$(".toolbar-btn")].find(b => b.textContent.includes("发简历"));
if (resumeBtn) { simulateClick(resumeBtn); await sleep(500); UI.log("📎 已发送简历"); }
}
}
}
await sleep(C.DELAYS.CHAT_CHECK);
}
},
};
// ═══════════════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════════════
function init() {
UI.init();
console.log("[禾急-BOSS] v" + C.VERSION + " loaded");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();