// ==UserScript== // @name 小怪的求职助手 // @namespace job-helper // @version 4.5.0 // @description BOSS直聘求职效率助手:职位筛选、批量沟通、AI 匹配与回复建议、简历解析及本地投递记录 // @match *://www.zhipin.com/web/* // @require https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js // @require https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js // @require https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js // @require https://cdn.jsdelivr.net/npm/tesseract.js@5.1.1/dist/tesseract.min.js // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_notification // @connect * // @run-at document-idle // @license MIT // ==/UserScript== (function () { "use strict"; /** * @typedef {Object} HRInteraction * @property {string} hrKey - HR唯一标识 * @property {boolean} hasGreeted - 是否已打招呼 * @property {boolean} hasSentResume - 是否已发送简历 * @property {boolean} hasSentImageResume - 是否已发送图片简历 */ /** * @typedef {Object} JobInfo * @property {string} jobId - 职位ID * @property {string} title - 职位标题 * @property {string} company - 公司名称 * @property {string} salary - 薪资范围 * @property {string} location - 工作地点 * @property {string} hrKey - HR标识 */ /** * @typedef {Object} GreetingItem * @property {string} id - 问候语ID * @property {string} content - 问候语内容 */ /** * @typedef {Object} ImageResume * @property {string} id - 图片简历ID * @property {string} name - 图片简历名称 * @property {string} data - Base64编码的图片数据 */ /** * @typedef {Object} ErrorInfo * @property {string} message - 错误消息 * @property {string} stack - 错误堆栈 * @property {string} context - 错误上下文 * @property {string} timestamp - 时间戳 */ const CONFIG = { BASIC_INTERVAL: 1000, OPERATION_INTERVAL: 1200, DELAYS: { SHORT: 30, MEDIUM_SHORT: 200, }, MINI_ICON_SIZE: 40, STORAGE_KEYS: { PROCESSED_HRS: "processedHRs", SENT_GREETINGS_HRS: "sentGreetingsHRs", SENT_RESUME_HRS: "sentResumeHRs", SENT_IMAGE_RESUME_HRS: "sentImageResumeHRs", AI_REPLY_COUNT: "aiReplyCount", LAST_AI_DATE: "lastAiDate", }, STORAGE_LIMITS: { PROCESSED_HRS: 500, SENT_GREETINGS_HRS: 500, SENT_RESUME_HRS: 300, SENT_IMAGE_RESUME_HRS: 300, }, API: { TIMEOUT: 30000, RETRY_COUNT: 3, RETRY_DELAY: 1000 }, UI: { MINI_ICON_SIZE: 40, ANIMATION_DURATION: 300, DEBOUNCE_DELAY: 300 }, PERFORMANCE: { DOM_CACHE_MAX_AGE: 5000, BATCH_SIZE: 10, CONCURRENT_LIMIT: 3 } }; const getStoredJSON = (key, defaultValue) => { try { const val = localStorage.getItem(key); return val ? JSON.parse(val) : defaultValue; } catch (e) { console.error(`Error parsing ${key}:`, e); return defaultValue; } }; // 安全地存储大文本到localStorage(自动截断) const setLargeItem = (key, value, maxLength = 500000) => { try { let textToStore = value; // 如果文本太长,截断它 if (textToStore && textToStore.length > maxLength) { console.warn(`文本太长(${textToStore.length}字符),已截断到${maxLength}字符`); textToStore = textToStore.substring(0, maxLength) + "\n[内容已截断,仅保存前" + maxLength + "字符]"; } const jsonString = JSON.stringify(textToStore); // 检查是否超过localStorage限制 if (jsonString.length > 2000000) { // 约2MB console.warn(`存储数据太大(${jsonString.length}字节),尝试进一步截断`); textToStore = textToStore.substring(0, Math.floor(maxLength / 2)) + "\n[内容已大幅截断以符合存储限制]"; } localStorage.setItem(key, JSON.stringify(textToStore)); return true; } catch (e) { if (e.name === 'QuotaExceededError' || e.message.includes('quota')) { console.error(`存储空间不足,无法保存${key}`); // 尝试保存截断版本 try { const truncated = String(value).substring(0, 100000) + "\n[因存储限制已截断]"; localStorage.setItem(key, JSON.stringify(truncated)); return 'truncated'; } catch (e2) { console.error(`即使截断后仍无法保存${key}`); return false; } } console.error(`Error saving ${key}:`, e); return false; } }; function parseAiCustomHeaders(rawHeaders) { if (!rawHeaders || !rawHeaders.trim()) return {}; let headers; try { headers = JSON.parse(rawHeaders); } catch (_) { throw new Error("自定义请求头必须是合法的 JSON 对象"); } if (!headers || Array.isArray(headers) || typeof headers !== "object") { throw new Error("自定义请求头必须是 JSON 对象"); } return Object.fromEntries(Object.entries(headers).map(([key, value]) => [String(key), String(value)])); } function hasHeader(headers, name) { return Object.keys(headers).some(key => key.toLowerCase() === name.toLowerCase()); } function formatAiEndpoint(apiUrl, model, apiKey, apiType) { let url = String(apiUrl || "").trim() .replaceAll("{model}", encodeURIComponent(model || "")) .replaceAll("{key}", encodeURIComponent(apiKey || "")); if (!url) throw new Error("请填写 API 地址"); if (apiType === "gemini" && apiKey && !/[?&]key=/.test(url) && !apiUrl.includes("{key}")) { url += (url.includes("?") ? "&" : "?") + "key=" + encodeURIComponent(apiKey); } return url; } function buildAiApiRequest({ apiType = "openai", apiUrl, apiKey, model, systemRole, message, maxTokens = 512, customHeaders = "" }) { const type = ["openai", "anthropic", "gemini"].includes(apiType) ? apiType : "openai"; const headers = Object.assign({ "Content-Type": "application/json" }, parseAiCustomHeaders(customHeaders)); const url = formatAiEndpoint(apiUrl, model, apiKey, type); if (type === "anthropic") { if (apiKey && !hasHeader(headers, "x-api-key")) headers["x-api-key"] = apiKey; if (!hasHeader(headers, "anthropic-version")) headers["anthropic-version"] = "2023-06-01"; return { url, headers, data: { model, max_tokens: maxTokens, system: systemRole, messages: [{ role: "user", content: message }] } }; } if (type === "gemini") { return { url, headers, data: { systemInstruction: { parts: [{ text: systemRole }] }, contents: [{ role: "user", parts: [{ text: message }] }], generationConfig: { temperature: 0.7, topP: 0.8, maxOutputTokens: maxTokens } } }; } if (apiKey && !hasHeader(headers, "authorization")) headers.Authorization = "Bearer " + apiKey; return { url, headers, data: { model, messages: [ { role: "system", content: systemRole }, { role: "user", content: message } ], max_tokens: maxTokens, temperature: 0.7, top_p: 0.8 } }; } function extractAiResponse(result, apiType = "openai") { const type = ["openai", "anthropic", "gemini"].includes(apiType) ? apiType : "openai"; let text = ""; if (type === "anthropic") { text = (result?.content || []).map(part => part?.text || "").join(""); } else if (type === "gemini") { text = (result?.candidates?.[0]?.content?.parts || []).map(part => part?.text || "").join(""); } else { const content = result?.choices?.[0]?.message?.content ?? result?.choices?.[0]?.text ?? result?.output_text; text = Array.isArray(content) ? content.map(part => part?.text || "").join("") : (content || ""); } if (typeof text === "string" && text.trim()) return text.trim(); const apiMessage = result?.error?.message || result?.message; if (apiMessage) throw new Error("API错误: " + apiMessage); throw new Error("无法解析 API 响应格式"); } const state = { isRunning: false, currentIndex: 0, currentCityIndex: 0, includeKeywords: [], locationKeywords: [], jobList: [], ui: { isMinimized: false, theme: localStorage.getItem("theme") || "light", }, hrInteractions: { processedHRs: new Set(getStoredJSON("processedHRs", [])), sentGreetingsHRs: new Set(getStoredJSON("sentGreetingsHRs", [])), sentResumeHRs: new Set(getStoredJSON("sentResumeHRs", [])), sentImageResumeHRs: new Set(getStoredJSON("sentImageResumeHRs", [])), lastMessageTime: getStoredJSON("lastMessageTime", {}), }, ai: { replyCount: getStoredJSON("aiReplyCount", 0), lastAiDate: localStorage.getItem("lastAiDate") || "", useAiReply: true, }, settings: { useAutoSendResume: getStoredJSON("useAutoSendResume", false), actionDelays: { click: parseInt(localStorage.getItem("clickDelay") || "130"), }, ai: { role: localStorage.getItem("aiRole") || "你是一个正在BOSS直聘上与HR聊天的求职者。回复要求:简短口语化(30字内),像朋友微信聊天;有经验说经验,没经验说能力;工资说范围、到岗给时间、离职说成长;不说您好、不模板化、不写小作文。", // 免费版本:用户自定义AI API配置 apiKey: localStorage.getItem("aiApiKey") || "", apiUrl: localStorage.getItem("aiApiUrl") || "https://api.openai.com/v1/chat/completions", model: localStorage.getItem("aiModel") || "gpt-4o-mini", apiType: localStorage.getItem("aiApiType") || "openai", customHeaders: localStorage.getItem("aiCustomHeaders") || "", useCustomApi: localStorage.getItem("useCustomApi") === "true", }, autoReply: getStoredJSON("autoReply", false), useAutoSendImageResume: getStoredJSON("useAutoSendImageResume", false), imageResumeData: localStorage.getItem("imageResumeData") || null, communicationMode: getStoredJSON("communicationMode", "new-only"), recruiterActivityStatus: getStoredJSON( "recruiterActivityStatus", ["不限"] ), excludeHeadhunters: getStoredJSON("excludeHeadhunters", false), salaryRange: { min: 0, max: 100 }, salaryFilter: "all", experienceFilter: ["不限"], educationFilter: ["不限"], candidateExperience: getStoredJSON("candidateExperience", ""), // 默认基础模式不依赖 AI 或简历;AI 匹配模式由用户主动开启。 deliveryMode: localStorage.getItem("deliveryMode") === "ai-match" ? "ai-match" : "basic", aiMatchThreshold: Math.min( 95, Math.max(50, parseInt(localStorage.getItem("aiMatchThreshold") || "70", 10) || 70) ), imageResumes: getStoredJSON("imageResumes", []), resumeText: getStoredJSON("resumeText", ""), resumeAnalysis: getStoredJSON("resumeAnalysis", ""), greetingsList: getStoredJSON("greetingsList", [ ]), }, comments: { currentCompanyName: "", commentsList: [], isLoading: false, isCommentMode: false, }, applicationRecords: getStoredJSON("applicationRecords", []), }; const elements = { panel: null, controlBtn: null, log: null, includeInput: null, locationInput: null, miniIcon: null, }; class ErrorHandler { static handle(error, context = '') { const errorInfo = { message: error.message, stack: error.stack, context, timestamp: new Date().toISOString() }; console.error(`[${context}]`, error); if (state.settings && state.settings.errorReporting) { this.report(errorInfo); } return errorInfo; } static async wrap(fn, context) { try { return await fn(); } catch (error) { return this.handle(error, context); } } static report(errorInfo) { console.log('Error reported:', errorInfo); } } class DOMCache { static cache = new Map(); static maxAge = CONFIG.PERFORMANCE.DOM_CACHE_MAX_AGE; static get(selector) { const cached = this.cache.get(selector); if (cached && Date.now() - cached.time < this.maxAge) { return cached.element; } const element = document.querySelector(selector); if (element) { this.cache.set(selector, { element, time: Date.now() }); } return element; } static getAll(selector) { return document.querySelectorAll(selector); } static clear() { this.cache.clear(); } static remove(selector) { this.cache.delete(selector); } } class ManagedSet { constructor(maxSize = 500) { this.items = new Set(); this.maxSize = maxSize; } add(item) { if (this.items.size >= this.maxSize) { const firstItem = this.items.values().next().value; this.items.delete(firstItem); } this.items.add(item); } has(item) { return this.items.has(item); } delete(item) { return this.items.delete(item); } clear() { this.items.clear(); } get size() { return this.items.size; } toArray() { return Array.from(this.items); } } class EventManager { static listeners = new Map(); static add(element, event, handler, options = {}) { const key = `${element.id || element.className || element.tagName}-${event}-${Date.now()}`; if (this.listeners.has(key)) { this.remove(key); } element.addEventListener(event, handler, options); this.listeners.set(key, { element, event, handler }); return key; } static remove(key) { const listener = this.listeners.get(key); if (listener) { listener.element.removeEventListener( listener.event, listener.handler ); this.listeners.delete(key); } } static removeAll() { this.listeners.forEach((_, key) => this.remove(key)); } static getByElement(element) { const results = []; this.listeners.forEach((listener, key) => { if (listener.element === element) { results.push({ key, ...listener }); } }); return results; } } class DOMUtils { static async waitForAndAct(selector, action, options = {}) { const { timeout = 5000, retryInterval = 100, maxRetries = 3 } = options; for (let i = 0; i < maxRetries; i++) { try { const element = await Core.waitForElement(selector, timeout); if (element) { const result = await action(element); return result; } } catch (error) { if (i === maxRetries - 1) throw error; await Core.delay(retryInterval); } } return null; } static async clickElement(selector, options = {}) { return this.waitForAndAct(selector, async (element) => { await Core.simulateClick(element); return true; }, options); } static async inputText(selector, text, options = {}) { return this.waitForAndAct(selector, async (element) => { element.textContent = ""; element.focus(); document.execCommand("insertText", false, text); return true; }, options); } static debounce(fn, delay = CONFIG.UI.DEBOUNCE_DELAY) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } static throttle(fn, delay = CONFIG.UI.DEBOUNCE_DELAY) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= delay) { lastTime = now; return fn.apply(this, args); } }; } } class StorageManager { static setItem(key, value) { try { localStorage.setItem( key, typeof value === "string" ? value : JSON.stringify(value) ); return true; } catch (error) { Core.log(`设置存储项 ${key} 失败: ${error.message}`); return false; } } static getItem(key, defaultValue = null) { try { const value = localStorage.getItem(key); return value !== null ? value : defaultValue; } catch (error) { Core.log(`获取存储项 ${key} 失败: ${error.message}`); return defaultValue; } } static removeItem(key) { try { localStorage.removeItem(key); return true; } catch (error) { Core.log(`删除存储项 ${key} 失败: ${error.message}`); return false; } } static addRecordWithLimit(storageKey, record, currentSet, limit) { try { if (currentSet.has(record)) { return; } let records = this.getParsedItem(storageKey, []); records = Array.isArray(records) ? records : []; if (records.length >= limit) { records.shift(); } records.push(record); currentSet.add(record); this.setItem(storageKey, records); console.log( `存储管理: 添加记录${records.length >= limit ? "并删除最早记录" : "" },当前${storageKey}数量: ${records.length}/${limit}` ); } catch (error) { console.log(`存储管理出错: ${error.message}`); } } static getParsedItem(storageKey, defaultValue = []) { try { const data = this.getItem(storageKey); return data ? JSON.parse(data) : defaultValue; } catch (error) { Core.log(`解析存储记录出错: ${error.message}`); return defaultValue; } } static ensureStorageLimits() { const limitConfigs = [ { key: CONFIG.STORAGE_KEYS.PROCESSED_HRS, set: state.hrInteractions.processedHRs, limit: CONFIG.STORAGE_LIMITS.PROCESSED_HRS, }, { key: CONFIG.STORAGE_KEYS.SENT_GREETINGS_HRS, set: state.hrInteractions.sentGreetingsHRs, limit: CONFIG.STORAGE_LIMITS.SENT_GREETINGS_HRS, }, { key: CONFIG.STORAGE_KEYS.SENT_RESUME_HRS, set: state.hrInteractions.sentResumeHRs, limit: CONFIG.STORAGE_LIMITS.SENT_RESUME_HRS, }, { key: CONFIG.STORAGE_KEYS.SENT_IMAGE_RESUME_HRS, set: state.hrInteractions.sentImageResumeHRs, limit: CONFIG.STORAGE_LIMITS.SENT_IMAGE_RESUME_HRS, }, ]; limitConfigs.forEach(({ key, set, limit }) => { const records = this.getParsedItem(key, []); if (records.length > limit) { const trimmedRecords = records.slice(-limit); this.setItem(key, trimmedRecords); set.clear(); trimmedRecords.forEach((record) => set.add(record)); console.log( `存储管理: 清理${key}记录,从${records.length}减少到${trimmedRecords.length}` ); } }); } } class StatePersistence { static saveState() { try { const stateMap = { aiReplyCount: state.ai.replyCount, lastAiDate: state.ai.lastAiDate, useAiReply: state.ai.useAiReply, useAutoSendResume: state.settings.useAutoSendResume, useAutoSendImageResume: state.settings.useAutoSendImageResume, imageResumeData: state.settings.imageResumeData, imageResumes: state.settings.imageResumes || [], greetingsList: state.settings.greetingsList || [], theme: state.ui.theme, clickDelay: state.settings.actionDelays.click, includeKeywords: state.includeKeywords, locationKeywords: state.locationKeywords, deliveryMode: state.settings.deliveryMode, aiMatchThreshold: state.settings.aiMatchThreshold, }; Object.entries(stateMap).forEach(([key, value]) => { StorageManager.setItem(key, value); }); } catch (error) { Core.log(`保存状态失败: ${error.message}`); } } static loadState() { try { state.includeKeywords = StorageManager.getParsedItem( "includeKeywords", [] ); state.locationKeywords = StorageManager.getParsedItem("locationKeywords") || StorageManager.getParsedItem("excludeKeywords", []); const imageResumes = StorageManager.getParsedItem("imageResumes", []); if (Array.isArray(imageResumes)) state.settings.imageResumes = imageResumes; const greetingsList = StorageManager.getParsedItem("greetingsList", []); if (Array.isArray(greetingsList)) state.settings.greetingsList = greetingsList; StorageManager.ensureStorageLimits(); } catch (error) { Core.log(`加载状态失败: ${error.message}`); } } } class HRInteractionManager { /** * 从聊天界面获取岗位名称 * @returns {string} */ static getPositionNameFromChat() { try { // 尝试多种选择器获取岗位名称 const selectors = [ '.chat-header .position-name', '.chat-header .job-name', '.chat-header .name', '.chat-basic-info .name', '.chat-basic-info .position-name', '.position-card .name', '.chat-top-info .name', '.chat-top-info .position-name' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element) { const text = element.textContent.trim(); if (text && text.length > 0) { return text; } } } // 尝试从聊天标题获取 const titleEl = document.querySelector('.chat-header-title, .chat-title'); if (titleEl) { const text = titleEl.textContent.trim(); if (text) return text; } return ''; } catch (e) { return ''; } } /** * 处理HR交互 * @param {string} hrKey - HR唯一标识 * @returns {Promise} */ static async handleHRInteraction(hrKey) { const hasResponded = await this.hasHRResponded(); if (!state.hrInteractions.sentGreetingsHRs.has(hrKey)) { await this._handleFirstInteraction(hrKey); return; } if ( !state.hrInteractions.sentResumeHRs.has(hrKey) || !state.hrInteractions.sentImageResumeHRs.has(hrKey) ) { if (hasResponded) { await this._handleFollowUpResponse(hrKey); } // 无论是否发送简历,都尝试AI回复 await Core.aiReply(); return; } // 所有流程完成后,调用AI回复 await Core.aiReply(); } static async _handleFirstInteraction(hrKey) { Core.log(`首次沟通: ${hrKey}`); const sentGreeting = await this.sendGreetings(); if (sentGreeting) { StorageManager.addRecordWithLimit( CONFIG.STORAGE_KEYS.SENT_GREETINGS_HRS, hrKey, state.hrInteractions.sentGreetingsHRs, CONFIG.STORAGE_LIMITS.SENT_GREETINGS_HRS ); await this._handleResumeSending(hrKey); } } static async _handleResumeSending(hrKey) { if ( state.settings.useAutoSendResume && !state.hrInteractions.sentResumeHRs.has(hrKey) ) { const sentResume = await this.sendResume(); if (sentResume) { StorageManager.addRecordWithLimit( CONFIG.STORAGE_KEYS.SENT_RESUME_HRS, hrKey, state.hrInteractions.sentResumeHRs, CONFIG.STORAGE_LIMITS.SENT_RESUME_HRS ); } } if ( state.settings.useAutoSendImageResume && !state.hrInteractions.sentImageResumeHRs.has(hrKey) ) { const sentImageResume = await this.sendImageResume(); if (sentImageResume) { StorageManager.addRecordWithLimit( CONFIG.STORAGE_KEYS.SENT_IMAGE_RESUME_HRS, hrKey, state.hrInteractions.sentImageResumeHRs, CONFIG.STORAGE_LIMITS.SENT_IMAGE_RESUME_HRS ); } } } static async _handleFollowUpResponse(hrKey) { if (this.hasCardMessage()) { const handled = await this.handleCardMessage(hrKey); if (handled) { return; } } const lastMessage = await Core.getLastFriendMessageText(); if ( lastMessage && (lastMessage.includes("简历") || lastMessage.includes("发送简历")) ) { Core.log(`HR提到"简历",发送简历: ${hrKey}`); if ( state.settings.useAutoSendImageResume && !state.hrInteractions.sentImageResumeHRs.has(hrKey) ) { const sentImageResume = await this.sendImageResume(); if (sentImageResume) { state.hrInteractions.sentImageResumeHRs.add(hrKey); StatePersistence.saveState(); Core.log(`已向 ${hrKey} 发送图片简历`); return; } } if (!state.hrInteractions.sentResumeHRs.has(hrKey)) { const sentResume = await this.sendResume(); if (sentResume) { state.hrInteractions.sentResumeHRs.add(hrKey); StatePersistence.saveState(); Core.log(`已向 ${hrKey} 发送简历`); } } } } /** * 发送自定义回复 * @param {string} replyText - 回复文本 * @returns {Promise} 是否发送成功 */ static async sendCustomReply(replyText) { try { const inputBox = await Core.waitForElement("#chat-input"); if (!inputBox) { Core.log("未找到聊天输入框"); return false; } inputBox.textContent = ""; inputBox.focus(); document.execCommand("insertText", false, replyText); await Core.delay(CONFIG.OPERATION_INTERVAL / 10); const sendButton = DOMCache.get(".btn-send"); if (sendButton) { await Core.simulateClick(sendButton); } else { const enterKeyEvent = new KeyboardEvent("keydown", { key: "Enter", keyCode: 13, code: "Enter", which: 13, bubbles: true, }); inputBox.dispatchEvent(enterKeyEvent); } return true; } catch (error) { ErrorHandler.handle(error, 'HRInteractionManager.sendCustomReply'); Core.log(`发送自定义回复出错: ${error.message}`); return false; } } static async hasHRResponded() { await Core.delay(state.settings.actionDelays.click); const chatContainer = DOMCache.get(".chat-message .im-list"); if (!chatContainer) return false; const friendMessages = Array.from( chatContainer.querySelectorAll("li.message-item.item-friend") ); return friendMessages.length > 0; } static hasCardMessage() { try { const chatContainer = DOMCache.get(".chat-message .im-list"); if (!chatContainer) return false; const friendMessages = Array.from( chatContainer.querySelectorAll("li.message-item.item-friend") ); if (friendMessages.length === 0) return false; const lastMessageEl = friendMessages[friendMessages.length - 1]; const cardWrap = lastMessageEl.querySelector(".message-card-wrap"); return cardWrap !== null; } catch (error) { Core.log(`检测卡片消息出错: ${error.message}`); return false; } } static async handleCardMessage(hrKey) { try { const chatContainer = DOMCache.get(".chat-message .im-list"); if (!chatContainer) { Core.log("未找到聊天容器"); return false; } const friendMessages = Array.from( chatContainer.querySelectorAll("li.message-item.item-friend") ); if (friendMessages.length === 0) { Core.log("未找到HR消息"); return false; } const lastMessageEl = friendMessages[friendMessages.length - 1]; const cardButtons = lastMessageEl.querySelectorAll(".card-btn"); if (!cardButtons || cardButtons.length === 0) { Core.log("未找到卡片按钮"); return false; } for (const btn of cardButtons) { if (btn.textContent.trim() === "同意") { await Core.simulateClick(btn); await Core.delay(state.settings.actionDelays.click); return true; } } Core.log(`未找到"同意"按钮`); return false; } catch (error) { Core.log(`处理卡片消息出错: ${error.message}`); return false; } } static async sendGreetings() { try { const greeting = Array.isArray(state.settings.greetingsList) ? state.settings.greetingsList.find((item) => item?.content?.trim()) : null; if (!greeting) return false; Core.log("发送自我介绍"); await this.sendCustomReply(greeting.content.trim()); await Core.delay(state.settings.actionDelays.click); return true; } catch (error) { Core.log(`发送自我介绍出错: ${error.message}`); return false; } } static _findMatchingResume(resumeItems, positionName) { try { const positionNameLower = positionName.toLowerCase(); const twoCharKeywords = Core.extractTwoCharKeywords(positionNameLower); for (const keyword of twoCharKeywords) { for (const item of resumeItems) { const resumeNameElement = item.querySelector(".resume-name"); if (!resumeNameElement) continue; const resumeName = resumeNameElement.textContent .trim() .toLowerCase(); if (resumeName.includes(keyword)) { const resumeNameText = resumeNameElement.textContent.trim(); Core.log(`智能匹配: "${resumeNameText}" 依据: "${keyword}"`); return item; } } } return null; } catch (error) { Core.log(`简历匹配出错: ${error.message}`); return null; } } static async sendResume() { try { const resumeBtn = await Core.waitForElement(() => { return [...document.querySelectorAll(".toolbar-btn")].find( (el) => el.textContent.trim() === "发简历" ); }); if (!resumeBtn) { Core.log("无法发送简历,未找到发简历按钮"); return false; } if (resumeBtn.classList.contains("unable")) { Core.log("对方未回复,您无权发送简历"); return false; } let positionName = Core.getPositionName(); if (!positionName) { Core.log("未找到岗位名称元素"); } await Core.simulateClick(resumeBtn); await Core.smartDelay(state.settings.actionDelays.click, "click"); await Core.smartDelay(800, "resume_load"); const confirmDialog = document.querySelector( ".panel-resume.sentence-popover" ); if (confirmDialog) { Core.log("您只有一份附件简历"); const confirmBtn = confirmDialog.querySelector(".btn-sure-v2"); if (!confirmBtn) { Core.log("未找到确认按钮"); return false; } await Core.simulateClick(confirmBtn); return true; } const resumeList = await Core.waitForElement("ul.resume-list"); if (!resumeList) { Core.log("未找到简历列表"); return false; } const resumeItems = Array.from( resumeList.querySelectorAll("li.list-item") ); if (resumeItems.length === 0) { Core.log("未找到简历列表项"); return false; } let selectedResumeItem = null; if (positionName) { selectedResumeItem = this._findMatchingResume( resumeItems, positionName ); } if (!selectedResumeItem) { selectedResumeItem = resumeItems[0]; const resumeName = selectedResumeItem .querySelector(".resume-name") .textContent.trim(); Core.log('使用第一个简历: "' + resumeName + '"'); } await Core.simulateClick(selectedResumeItem); await Core.smartDelay(state.settings.actionDelays.click, "click"); await Core.smartDelay(500, "selection"); const sendBtn = await Core.waitForElement( "button.btn-v2.btn-sure-v2.btn-confirm" ); if (!sendBtn) { Core.log("未找到发送按钮"); return false; } if (sendBtn.disabled) { Core.log("发送按钮不可用,可能简历未正确选择"); return false; } await Core.simulateClick(sendBtn); return true; } catch (error) { Core.log(`发送简历出错: ${error.message}`); return false; } } static selectImageResume(positionName) { try { const positionNameLower = positionName.toLowerCase(); if (state.settings.imageResumes.length === 1) { return state.settings.imageResumes[0]; } const twoCharKeywords = Core.extractTwoCharKeywords(positionNameLower); for (const keyword of twoCharKeywords) { for (const resume of state.settings.imageResumes) { const resumeNameLower = resume.path.toLowerCase(); if (resumeNameLower.includes(keyword)) { Core.log(`智能匹配: "${resume.path}" 依据: "${keyword}"`); return resume; } } } return state.settings.imageResumes[0]; } catch (error) { Core.log(`选择图片简历出错: ${error.message}`); return state.settings.imageResumes[0] || null; } } static async sendImageResume() { try { if ( !state.settings.useAutoSendImageResume || !state.settings.imageResumes || state.settings.imageResumes.length === 0 ) { return false; } let positionName = Core.getPositionName(); if (!positionName) { Core.log("未找到岗位名称元素"); } const imageSendBtn = await Core.waitForElement( '.toolbar-btn-content.icon.btn-sendimg input[type="file"]' ); if (!imageSendBtn) { Core.log("未找到图片发送按钮"); return false; } // 发送所有图片简历 let sentCount = 0; for (const resume of state.settings.imageResumes) { if (!resume || !resume.data) { Core.log(`跳过无效简历: ${resume?.path || 'unknown'}`); continue; } try { const byteCharacters = atob(resume.data.split(",")[1]); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: "image/jpeg" }); const file = new File([blob], resume.path, { type: "image/jpeg", lastModified: new Date().getTime(), }); const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); imageSendBtn.files = dataTransfer.files; const event = new Event("change", { bubbles: true }); imageSendBtn.dispatchEvent(event); sentCount++; Core.log(`已发送图片简历: ${resume.path}`); // 等待一下再发送下一张,避免过快 if (sentCount < state.settings.imageResumes.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } } catch (err) { Core.log(`发送图片简历失败 ${resume.path}: ${err.message}`); } } Core.log(`共发送 ${sentCount} 张图片简历`); return sentCount > 0; } catch (error) { Core.log(`发送图片出错: ${error.message}`); return false; } } } // \u7edf\u4e00\u4f7f\u7528\u8f7b\u91cf\u5185\u8054 SVG \u56fe\u6807\uff0c\u4e0d\u4f9d\u8d56\u7ad9\u70b9\u5b57\u4f53\u56fe\u6807\u5e93\u6216\u7cfb\u7edf Emoji \u663e\u793a\u3002 const APP_ICONS = { sparkles: '', settings: '', chart: '', close: '', file: '', chat: '', info: '', briefcase: '', check: '', alert: '', refresh: '' }; const UI = { PAGE_TYPES: { JOB_LIST: "jobList", CHAT: "chat", }, currentPageType: null, init() { this.currentPageType = location.pathname.includes("/chat") ? this.PAGE_TYPES.CHAT : this.PAGE_TYPES.JOB_LIST; this._applyTheme(); this.createControlPanel(); this.createMiniIcon(); this.setupJobCardClickListener(); }, setupJobCardClickListener() { // 评论功能已移除 }, createControlPanel() { if (document.getElementById("boss-pro-panel")) { document.getElementById("boss-pro-panel").remove(); } elements.panel = this._createPanel(); const header = this._createHeader(); const controls = this._createPageControls(); elements.log = this._createLogger(); const footer = this._createFooter(); elements.panel.append(header, controls, elements.log, footer); document.body.appendChild(elements.panel); this._makeDraggable(elements.panel); }, _applyTheme() { CONFIG.COLORS = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? this.THEMES.JOB_LIST : this.THEMES.CHAT; document.documentElement.style.setProperty( "--primary-color", CONFIG.COLORS.primary ); document.documentElement.style.setProperty( "--secondary-color", CONFIG.COLORS.secondary ); document.documentElement.style.setProperty( "--accent-color", CONFIG.COLORS.accent ); document.documentElement.style.setProperty( "--neutral-color", CONFIG.COLORS.neutral ); }, THEMES: { JOB_LIST: { primary: "#4285f4", secondary: "#f5f7fa", accent: "#e8f0fe", neutral: "#6b7280", }, CHAT: { primary: "#34a853", secondary: "#f0fdf4", accent: "#dcfce7", neutral: "#6b7280", }, }, _createPanel() { const panel = document.createElement("div"); panel.id = "boss-pro-panel"; panel.className = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? "boss-joblist-panel" : "boss-chat-panel"; const baseStyles = ` position: fixed; top: max(12px, env(safe-area-inset-top, 12px)); right: max(12px, env(safe-area-inset-right, 12px)); width: min(420px, calc(100vw - 24px)) !important; max-width: calc(100vw - 24px) !important; min-width: 0; max-height: calc(100vh - 24px); box-sizing: border-box; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; border-radius: 12px; padding: clamp(10px, 1.5vw, 16px); font-family: 'Segoe UI', system-ui, sans-serif; z-index: 2147483647; display: flex; flex-direction: column; transition: all 0.3s ease; background: #ffffff; box-shadow: 0 10px 25px rgba(var(--primary-rgb), 0.15); border: 1px solid var(--accent-color); cursor: default; `; panel.style.cssText = baseStyles; const rgbColor = this._hexToRgb(CONFIG.COLORS.primary); document.documentElement.style.setProperty("--primary-rgb", rgbColor); return panel; }, _createHeader() { const header = document.createElement("div"); header.className = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? "boss-header" : "boss-chat-header"; header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; padding: 0 10px 12px; margin-bottom: 12px; border-bottom: 1px solid var(--accent-color); `; const title = this._createTitle(); const buttonContainer = document.createElement("div"); buttonContainer.style.cssText = ` display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; `; const buttonTitles = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? { ai: "AI配置", settings: "插件设置", close: "最小化海投面板", } : { ai: "AI配置", settings: "海投设置", close: "最小化聊天面板", }; // AI配置按钮图标(使用机器人/AI图标) const aiConfigIcon = APP_ICONS.sparkles; const aiConfigBtn = this._createIconButton( aiConfigIcon, () => { showAIConfigDialog(); }, buttonTitles.ai ); aiConfigBtn.title = "AI 服务配置"; const settingsBtn = this._createIconButton( APP_ICONS.settings, () => { showSettingsDialog(); }, buttonTitles.settings ); const closeBtn = this._createIconButton( APP_ICONS.close, () => { state.isMinimized = true; elements.panel.style.transform = "translateY(160%)"; elements.miniIcon.style.display = "flex"; }, buttonTitles.close ); // 数据看板按钮 const dashboardBtn = this._createIconButton( APP_ICONS.chart, () => { toggleInlineDashboard(); }, "数据看板" ); dashboardBtn.title = "数据看板"; buttonContainer.append(aiConfigBtn, settingsBtn, dashboardBtn, closeBtn); header.append(title, buttonContainer); return header; }, _createTitle() { const title = document.createElement("div"); title.style.display = "flex"; title.style.alignItems = "center"; title.style.gap = "10px"; title.style.minWidth = "0"; title.style.flex = "1 1 170px"; const customSvg = ` `; const titleConfig = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? { main: "小怪的求职助手", sub: "筛选岗位 · 高效沟通", } : { main: "小怪的智能对话", sub: "生成建议 · 人工确认", }; title.innerHTML = `
${customSvg}

${titleConfig.main}

${titleConfig.sub}
`; return title; }, _createPageControls() { if (this.currentPageType === this.PAGE_TYPES.JOB_LIST) { return this._createJobListControls(); } else { return this._createChatControls(); } }, _createJobListControls() { const container = document.createElement("div"); container.className = "boss-joblist-controls"; container.style.marginBottom = "15px"; container.style.padding = "0 10px"; const filterContainer = this._createFilterContainer(); container.append(filterContainer); return container; }, _createChatControls() { const container = document.createElement("div"); container.className = "boss-chat-controls"; container.style.cssText = ` background: #f8fafc; border: 1px solid #dbeafe; border-radius: 14px; padding: 14px; margin: 0 10px 14px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04); box-sizing: border-box; `; const chatHeading = document.createElement("div"); chatHeading.style.cssText = "display:flex; align-items:flex-start; gap:10px; margin-bottom:12px; padding-bottom:10px; border-bottom:1px solid #dbeafe;"; const chatHeadingIcon = document.createElement("span"); chatHeadingIcon.innerHTML = APP_ICONS.chat; chatHeadingIcon.style.cssText = "display:flex; align-items:center; justify-content:center; width:32px; height:32px; flex:0 0 32px; border-radius:9px; color:#2563eb; background:#dbeafe;"; const chatHeadingCopy = document.createElement("div"); chatHeadingCopy.style.cssText = "min-width:0;"; const chatHeadingTitle = document.createElement("div"); chatHeadingTitle.textContent = "AI \u667a\u80fd\u5bf9\u8bdd"; chatHeadingTitle.style.cssText = "font-size:15px; font-weight:700; color:#0f172a; margin-bottom:3px;"; const chatHeadingHint = document.createElement("div"); chatHeadingHint.textContent = "\u6839\u636e\u60a8\u7684\u8bbe\u7f6e\u5904\u7406 HR \u65b0\u6d88\u606f\uff0c\u53ef\u5148\u751f\u6210\u5efa\u8bae\u518d\u53d1\u9001\u3002"; chatHeadingHint.style.cssText = "font-size:12px; line-height:1.45; color:#64748b;"; chatHeadingCopy.append(chatHeadingTitle, chatHeadingHint); chatHeading.append(chatHeadingIcon, chatHeadingCopy); const configRow = document.createElement("div"); configRow.style.cssText = ` display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin-bottom: 10px; `; const communicationIncludeCol = this._createInputControl( "\u6c9f\u901a\u5c97\u4f4d\u8303\u56f4\uff08\u53ef\u9009\uff09", "communication-include", "\u5982\uff1a\u5b89\u5168\u3001\u8fd0\u7ef4\u3001\u5f00\u53d1" ); const communicationModeCol = this._createSelectControl( "\u5bf9\u8bdd\u5904\u7406\u65b9\u5f0f", "communication-mode-selector", [ { value: "new-only", text: "\u4ec5\u5904\u7406\u65b0\u6d88\u606f" }, { value: "suggested-reply", text: "\u751f\u6210\u5efa\u8bae\uff0c\u4eba\u5de5\u786e\u8ba4" }, ] ); elements.communicationIncludeInput = communicationIncludeCol.querySelector("input"); elements.communicationModeSelector = communicationModeCol.querySelector("select"); elements.communicationIncludeInput.title = "\u591a\u4e2a\u5c97\u4f4d\u5173\u952e\u8bcd\u53ef\u7528\u9017\u53f7\u5206\u9694"; configRow.append(communicationIncludeCol, communicationModeCol); elements.communicationModeSelector.value = settings.communicationMode || "new-only"; elements.communicationModeSelector.addEventListener("change", (e) => { settings.communicationMode = e.target.value; if (e.target.value === "suggested-reply") { Core.log("\u5df2\u5207\u6362\u4e3a\u5efa\u8bae\u56de\u590d\uff1aAI \u4ec5\u751f\u6210\u5185\u5bb9\uff0c\u9700\u60a8\u786e\u8ba4\u540e\u53d1\u9001\u3002"); } saveSettings(); }); elements.communicationIncludeInput.addEventListener("input", (e) => { settings.communicationIncludeKeywords = e.target.value; saveSettings(); }); const safetyNotice = document.createElement("div"); safetyNotice.style.cssText = "display:flex; align-items:flex-start; gap:8px; margin:0 0 12px; padding:9px 10px; border:1px solid #bfdbfe; border-radius:9px; background:#eff6ff; color:#1e40af; font-size:12px; line-height:1.5;"; const safetyIcon = document.createElement("span"); safetyIcon.innerHTML = APP_ICONS.info; safetyIcon.style.cssText = "display:flex; flex:0 0 auto; margin-top:1px;"; const safetyText = document.createElement("span"); safetyText.textContent = "\u9ed8\u8ba4\u4e0d\u4f1a\u4e3b\u52a8\u53d1\u9001\u7b80\u5386\u3002\u4ec5\u5728\u60a8\u5728\u8bbe\u7f6e\u4e2d\u5f00\u542f\u201c\u81ea\u52a8\u53d1\u9001\u7b80\u5386\u201d\u4e14\u6ee1\u8db3\u89e6\u53d1\u6761\u4ef6\u65f6\uff0c\u7cfb\u7edf\u624d\u4f1a\u6267\u884c\u3002"; safetyNotice.append(safetyIcon, safetyText); elements.controlBtn = this._createTextButton( "\u5f00\u542f\u667a\u80fd\u5bf9\u8bdd", "var(--primary-color)", () => { toggleChatProcess(); } ); elements.controlBtn.style.cssText += "width:100%; padding:11px 14px; border-radius:9px; font-size:14px; font-weight:700; box-sizing:border-box;"; container.append(chatHeading, configRow, safetyNotice, elements.controlBtn); return container; }, _createFilterContainer() { const filterContainer = document.createElement("div"); filterContainer.style.cssText = ` background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 14px; padding: 14px; margin-bottom: 0; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04); `; const titleRow = document.createElement("div"); titleRow.style.cssText = ` font-size: 15px; font-weight: 700; color: #0f172a; margin-bottom: 12px; padding-bottom: 9px; border-bottom: 1px solid #e2e8f0; letter-spacing: 0.02em; `; titleRow.textContent = "筛选与投递"; const deliveryModeRow = document.createElement("div"); deliveryModeRow.style.cssText = "display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; width: 100%; box-sizing: border-box; margin: 0 0 12px; padding: 11px 12px; border: 1px solid #dbeafe; border-radius: 10px; background: #ffffff;"; const deliveryModeInfo = document.createElement("div"); deliveryModeInfo.style.cssText = "width: 100%; min-width: 0;"; const deliveryModeLabel = document.createElement("div"); deliveryModeLabel.textContent = "批量投递方式"; deliveryModeLabel.style.cssText = "font-size: 13px; font-weight: 700; color: #1e3a5f; margin-bottom: 3px;"; const deliveryModeHint = document.createElement("div"); deliveryModeHint.style.cssText = "font-size: 12px; line-height: 1.45; color: #64748b;"; deliveryModeInfo.append(deliveryModeLabel, deliveryModeHint); const deliveryModeControls = document.createElement("div"); deliveryModeControls.style.cssText = "display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; width: 100%; min-width: 0;"; const deliveryModeSelect = document.createElement("select"); deliveryModeSelect.id = "delivery-mode-select"; deliveryModeSelect.style.cssText = "width: 100% !important; max-width: 100%; min-width: 0; box-sizing: border-box; padding: 8px 10px; border: 1px solid #bfdbfe; border-radius: 8px; background: #fff; color: #1e3a5f; font-size: 13px; font-weight: 600; outline: none;"; deliveryModeSelect.innerHTML = ""; deliveryModeSelect.value = settings.deliveryMode === "ai-match" ? "ai-match" : "basic"; const thresholdControl = document.createElement("label"); thresholdControl.style.cssText = "display: flex; align-items: center; gap: 6px; color: #475569; font-size: 12px; white-space: nowrap;"; thresholdControl.textContent = "阈值"; const thresholdInput = document.createElement("input"); thresholdInput.type = "number"; thresholdInput.min = "50"; thresholdInput.max = "95"; thresholdInput.step = "1"; thresholdInput.value = String(settings.aiMatchThreshold); thresholdInput.style.cssText = "width: 48px; padding: 7px 6px; border: 1px solid #bfdbfe; border-radius: 7px; text-align: center; color: #1d4ed8; font-weight: 700; outline: none;"; const scoreUnit = document.createElement("span"); scoreUnit.textContent = "分"; thresholdControl.append(thresholdInput, scoreUnit); deliveryModeControls.append(deliveryModeSelect, thresholdControl); deliveryModeRow.append(deliveryModeInfo, deliveryModeControls); const syncDeliveryMode = (notify) => { settings.deliveryMode = deliveryModeSelect.value === "ai-match" ? "ai-match" : "basic"; settings.aiMatchThreshold = Math.min(95, Math.max(50, parseInt(thresholdInput.value, 10) || 70)); thresholdInput.value = String(settings.aiMatchThreshold); thresholdControl.style.display = settings.deliveryMode === "ai-match" ? "flex" : "none"; deliveryModeHint.textContent = settings.deliveryMode === "ai-match" ? "仅匹配分数达标的岗位才会沟通;会使用已保存的简历与 AI 配置。" : "不需要 AI 或简历,直接按下方筛选条件批量沟通。"; saveSettings(); StatePersistence.saveState(); if (notify) { showNotification(settings.deliveryMode === "ai-match" ? "已启用 AI 匹配投递" : "已切换为基础批量沟通", "success"); } }; deliveryModeSelect.addEventListener("change", () => syncDeliveryMode(true)); thresholdInput.addEventListener("change", () => { syncDeliveryMode(false); showNotification("AI 匹配阈值已设为 " + settings.aiMatchThreshold + " 分", "success"); }); syncDeliveryMode(false); const inputRow = document.createElement("div"); inputRow.style.cssText = ` display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 10px; `; const includeFilterCol = this._createInputControl( "职位名包含(可多个)", "include-filter", "多个关键词用逗号分隔,如:安全服务、安全运维、安全运营、网络安全" ); const locationFilterCol = this._createInputControl( "工作地包含", "location-filter", "如:北京、杭州" ); elements.includeInput = includeFilterCol.querySelector("input"); elements.locationInput = locationFilterCol.querySelector("input"); elements.includeInput.title = "可输入多个职位关键词,使用逗号、顿号、分号或换行分隔"; inputRow.append(includeFilterCol, locationFilterCol); const selectRow = document.createElement("div"); selectRow.style.cssText = ` display: grid; grid-template-columns: repeat(auto-fit, minmax(104px, 1fr)); gap: 10px; margin-bottom: 10px; `; const expSelect = document.createElement("select"); expSelect.id = "experience-select"; expSelect.style.cssText = ` width: 100%; box-sizing: border-box; min-width: 0; padding: 10px 12px; border-radius: 8px; border: 1px solid #d1d5db; font-size: 14px; background: white; color: #333; cursor: pointer; `; const experienceOptions = [ { value: "不限", text: "经验不限", bossText: "不限" }, { value: "应届生", text: "应届生", bossText: "应届生" }, { value: "1年以内", text: "1年以内", bossText: "1年以内" }, { value: "1-3年", text: "1-3年", bossText: "1-3年" }, { value: "3-5年", text: "3-5年", bossText: "3-5年" }, { value: "5-10年", text: "5-10年", bossText: "5-10年" }, { value: "10年以上", text: "10年以上", bossText: "10年以上" }, ]; experienceOptions.forEach((opt) => { const o = document.createElement("option"); o.value = opt.value; o.textContent = opt.text; expSelect.appendChild(o); }); // 从 localStorage 读取保存的经验筛选值 expSelect.value = "不限"; const expFilterCol = document.createElement("div"); const expLabel = document.createElement("label"); expLabel.textContent = "工作经验"; expLabel.style.cssText = "display:block; margin-bottom:5px; font-weight: 500; color: #333; font-size: 0.85rem;"; expFilterCol.appendChild(expLabel); expFilterCol.appendChild(expSelect); const clickBossExpFilter = async (bossText) => { try { const expFilterBtn = Array.from(document.querySelectorAll('.filter-select-box, .filter-item, [class*="filter"], button, div')) .find(el => el.textContent?.includes('经验') || el.textContent?.includes('工作年限')); if (expFilterBtn) { expFilterBtn.click(); await new Promise(resolve => setTimeout(resolve, 500)); const options = document.querySelectorAll('.filter-select-dropdown li, .dropdown-item, [class*="option"], li'); for (const option of options) { if (option.textContent?.includes(bossText)) { option.click(); Core.log(`已选择经验: ${bossText}`); return true; } } } return false; } catch (error) { console.error('点击经验筛选失败:', error); return false; } }; expSelect.addEventListener("change", async (e) => { const selected = experienceOptions.find(opt => opt.value === e.target.value); // 保存到 localStorage if (selected && selected.value !== "不限") { await clickBossExpFilter(selected.bossText); } }); const eduSelect = document.createElement("select"); eduSelect.id = "education-select"; eduSelect.style.cssText = ` width: 100%; box-sizing: border-box; min-width: 0; padding: 10px 12px; border-radius: 8px; border: 1px solid #d1d5db; font-size: 14px; background: white; color: #333; cursor: pointer; `; const educationOptions = [ { value: "不限", text: "学历不限", bossText: "不限" }, { value: "初中及以下", text: "初中及以下", bossText: "初中及以下" }, { value: "中专/中技", text: "中专/中技", bossText: "中专/中技" }, { value: "高中", text: "高中", bossText: "高中" }, { value: "大专", text: "大专", bossText: "大专" }, { value: "本科", text: "本科", bossText: "本科" }, { value: "硕士", text: "硕士", bossText: "硕士" }, { value: "博士", text: "博士", bossText: "博士" }, ]; educationOptions.forEach((opt) => { const o = document.createElement("option"); o.value = opt.value; o.textContent = opt.text; eduSelect.appendChild(o); }); // 从 localStorage 读取保存的学历筛选值 eduSelect.value = "不限"; const eduFilterCol = document.createElement("div"); const eduLabel = document.createElement("label"); eduLabel.textContent = "学历要求"; eduLabel.style.cssText = "display:block; margin-bottom:5px; font-weight: 500; color: #333; font-size: 0.85rem;"; eduFilterCol.appendChild(eduLabel); eduFilterCol.appendChild(eduSelect); const clickBossEduFilter = async (bossText) => { try { const eduFilterBtn = Array.from(document.querySelectorAll('.filter-select-box, .filter-item, [class*="filter"], button, div')) .find(el => el.textContent?.includes('学历') || el.textContent?.includes('教育')); if (eduFilterBtn) { eduFilterBtn.click(); await new Promise(resolve => setTimeout(resolve, 500)); const options = document.querySelectorAll('.filter-select-dropdown li, .dropdown-item, [class*="option"], li'); for (const option of options) { if (option.textContent?.includes(bossText)) { option.click(); Core.log(`已选择学历: ${bossText}`); return true; } } } return false; } catch (error) { console.error('点击学历筛选失败:', error); return false; } }; eduSelect.addEventListener("change", async (e) => { const selected = educationOptions.find(opt => opt.value === e.target.value); // 保存到 localStorage if (selected && selected.value !== "不限") { await clickBossEduFilter(selected.bossText); } }); const salarySelect = document.createElement("select"); salarySelect.id = "salary-filter"; salarySelect.style.cssText = ` width: 100%; box-sizing: border-box; min-width: 0; padding: 10px 12px; border-radius: 8px; border: 1px solid #d1d5db; font-size: 14px; background: white; color: #333; cursor: pointer; `; const salaryOptions = [ { value: "all", text: "薪资不限", bossText: "不限" }, { value: "below-3k", text: "3K以下", bossText: "3K以下" }, { value: "3-5k", text: "3-5K", bossText: "3-5K" }, { value: "5-10k", text: "5-10K", bossText: "5-10K" }, { value: "10-20k", text: "10-20K", bossText: "10-20K" }, { value: "20-50k", text: "20-50K", bossText: "20-50K" }, { value: "50k+", text: "50K以上", bossText: "50K以上" }, ]; salaryOptions.forEach((opt) => { const o = document.createElement("option"); o.value = opt.value; o.textContent = opt.text; salarySelect.appendChild(o); }); salarySelect.value = settings.salaryFilter || "all"; const salaryFilterCol = document.createElement("div"); const salaryLabel = document.createElement("label"); salaryLabel.textContent = "薪资范围"; salaryLabel.style.cssText = "display:block; margin-bottom:5px; font-weight: 500; color: #333; font-size: 0.85rem;"; salaryFilterCol.appendChild(salaryLabel); salaryFilterCol.appendChild(salarySelect); const clickBossSalaryFilter = async (bossText) => { try { const salaryFilterBtn = Array.from(document.querySelectorAll('.filter-select-box, .filter-item, [class*="filter"], button, div')) .find(el => el.textContent?.includes('薪资') || el.textContent?.includes('薪资待遇')); if (salaryFilterBtn) { salaryFilterBtn.click(); await new Promise(resolve => setTimeout(resolve, 500)); const options = document.querySelectorAll('.filter-select-dropdown li, .dropdown-item, [class*="option"], li'); for (const option of options) { if (option.textContent?.includes(bossText)) { option.click(); Core.log(`已选择薪资: ${bossText}`); return true; } } } return false; } catch (error) { console.error('点击薪资筛选失败:', error); return false; } }; salarySelect.addEventListener("change", async (e) => { const selectedOption = salaryOptions.find(opt => opt.value === e.target.value); if (selectedOption) { settings.salaryFilter = selectedOption.value; if (selectedOption.value !== "all") { await clickBossSalaryFilter(selectedOption.bossText); } } }); const activitySelect = document.createElement("select"); activitySelect.id = "recruiter-activity-select"; activitySelect.style.cssText = ` width: 100%; box-sizing: border-box; min-width: 0; padding: 10px 12px; border-radius: 8px; border: 1px solid #d1d5db; font-size: 14px; background: white; color: #333; cursor: pointer; `; const activityOptions = [ { value: "不限", text: "活跃度不限" }, { value: "今日活跃", text: "今日活跃" }, { value: "3日内活跃", text: "3日内活跃" }, { value: "本月活跃", text: "本月活跃" }, ]; activityOptions.forEach((opt) => { const option = document.createElement("option"); option.value = opt.value; option.textContent = opt.text; activitySelect.appendChild(option); }); const savedActivity = Array.isArray(settings.recruiterActivityStatus) ? settings.recruiterActivityStatus.find((value) => activityOptions.some((option) => option.value === value) ) : ""; activitySelect.value = savedActivity || "不限"; // 旧版多选配置中若保存了其他活跃状态,与首页单选 UI 保持一致。 if (!savedActivity) { settings.recruiterActivityStatus = [activitySelect.value]; saveSettings(); } // 独立展示活跃度条件,避免在窄屏下被与其他下拉框挤到靠后位置。 const activityFilterRow = document.createElement("div"); activityFilterRow.id = "recruiter-activity-filter-row"; activityFilterRow.style.cssText = "display:grid; grid-template-columns:minmax(0, 1fr) minmax(132px, 42%); align-items:center; gap:12px; box-sizing:border-box; margin:0 0 10px; padding:10px 12px; border:1px solid #cfe0ff; border-radius:10px; background:linear-gradient(135deg, #eff6ff, #ffffff);"; const activityInfo = document.createElement("div"); activityInfo.style.cssText = "min-width:0;"; const activityLabel = document.createElement("div"); activityLabel.textContent = "招聘者活跃度"; activityLabel.style.cssText = "font-size:13px; font-weight:700; color:#1e3a5f; margin-bottom:3px;"; const activityHint = document.createElement("div"); activityHint.textContent = "仅与活跃度符合的招聘者沟通"; activityHint.style.cssText = "font-size:11px; line-height:1.35; color:#64748b; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;"; activityInfo.append(activityLabel, activityHint); activitySelect.style.borderColor = "#93c5fd"; activitySelect.style.fontWeight = "600"; activitySelect.style.color = "#1d4ed8"; activityFilterRow.append(activityInfo, activitySelect); activitySelect.addEventListener("change", (event) => { settings.recruiterActivityStatus = [event.target.value]; saveSettings(); Core.log(`招聘者活跃度筛选已设为:${event.target.options[event.target.selectedIndex].text}`); }); selectRow.append(expFilterCol, eduFilterCol, salaryFilterCol); elements.controlBtn = this._createTextButton( "启动海投", "var(--primary-color)", () => { toggleProcess(); } ); elements.controlBtn.style.cssText += ` width: 100%; padding: 12px; font-size: 15px; font-weight: 600; border-radius: 10px; margin-top: 5px; `; // 活跃度筛选放在其他下拉条件之前,首页打开即可见。 filterContainer.append(titleRow, deliveryModeRow, inputRow, activityFilterRow, selectRow, elements.controlBtn); return filterContainer; }, _createInputControl(labelText, id, placeholder) { const controlCol = document.createElement("div"); controlCol.style.cssText = "flex: 1; min-width: 0;"; const label = document.createElement("label"); label.textContent = labelText; label.style.cssText = "display:block; margin-bottom:5px; font-weight: 500; color: #333; font-size: 0.9rem;"; const input = document.createElement("input"); input.id = id; input.placeholder = placeholder; input.style.cssText = ` width: 100%; min-width: 0; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid #d1d5db; font-size: 14px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); transition: all 0.2s ease; `; controlCol.append(label, input); return controlCol; }, _createSelectControl(labelText, id, options) { const controlCol = document.createElement("div"); controlCol.style.cssText = "flex: 1; min-width: 0;"; const label = document.createElement("label"); label.textContent = labelText; label.style.cssText = "display:block; margin-bottom:5px; font-weight: 500; color: #333; font-size: 0.9rem;"; const select = document.createElement("select"); select.id = id; select.style.cssText = ` width: 100%; min-width: 0; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid #d1d5db; font-size: 14px; background: white; color: #333; box-shadow: 0 1px 2px rgba(0,0,0,0.05); transition: all 0.2s ease; `; options.forEach((option) => { const opt = document.createElement("option"); opt.value = option.value; opt.textContent = option.text; select.appendChild(opt); }); controlCol.append(label, select); return controlCol; }, _createLogger() { const log = document.createElement("div"); log.id = "pro-log"; log.className = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? "boss-joblist-log" : "boss-chat-log"; const height = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? "260px" : "260px"; log.style.cssText = ` height: clamp(120px, 18vh, 180px); max-height: 180px; box-sizing: border-box; overflow-y: auto; background: var(--secondary-color); border-radius: 12px; padding: 12px; font-size: 13px; line-height: 1.5; margin-bottom: 15px; margin-left: 10px; margin-right: 10px; transition: all 0.3s ease; user-select: text; scrollbar-width: thin; scrollbar-color: var(--primary-color) var(--secondary-color); `; log.innerHTML += ` `; return log; }, _createFooter() { const footer = document.createElement("div"); footer.className = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? "boss-joblist-footer" : "boss-chat-footer"; footer.style.cssText = ` text-align: center; font-size: 0.8em; color: var(--neutral-color); padding-top: 15px; border-top: 1px solid var(--accent-color); margin-top: auto; padding: 0px; `; const statsContainer = document.createElement("div"); statsContainer.style.cssText = ` display: flex; justify-content: space-around; margin-bottom: 15px; `; const rewardTip = document.createElement("div"); rewardTip.style.cssText = ` margin-bottom: 10px; padding: 8px 12px; background: linear-gradient(135deg, #fff5f5 0%, #fff0f0 100%); border-radius: 8px; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 8px; `; footer.append( statsContainer, document.createTextNode("本地数据仅保存在当前浏览器") ); return footer; }, _createTextButton(text, bgColor, onClick) { const btn = document.createElement("button"); btn.className = "boss-btn"; btn.textContent = text; btn.style.cssText = ` width: 100%; padding: 10px 16px; background: ${bgColor}; color: #fff; border: none; border-radius: 10px; cursor: pointer; font-size: 15px; font-weight: 500; transition: all 0.3s ease; display: flex; justify-content: center; align-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.1); transform: translateY(0px); margin: 0 auto; `; this._addButtonHoverEffects(btn); btn.addEventListener("click", onClick); return btn; }, _createIconButton(icon, onClick, title) { const btn = document.createElement("button"); btn.className = "boss-icon-btn"; btn.innerHTML = icon; btn.title = title; btn.style.cssText = ` width: 32px; height: 32px; border-radius: 50%; border: none; background: ${this.currentPageType === this.PAGE_TYPES.JOB_LIST ? "var(--accent-color)" : "var(--accent-color)" }; cursor: pointer; font-size: 16px; transition: all 0.2s ease; display: flex; justify-content: center; align-items: center; color: var(--primary-color); overflow: hidden; opacity: 1; `; if (icon.includes(" { btn.style.backgroundColor = "var(--primary-color)"; btn.style.color = "#fff"; btn.style.transform = "scale(1.1)"; if (icon.includes(" { btn.style.backgroundColor = this.currentPageType === this.PAGE_TYPES.JOB_LIST ? "var(--accent-color)" : "var(--accent-color)"; btn.style.color = "var(--primary-color)"; btn.style.transform = "scale(1)"; // 如果按钮包含 SVG,恢复 SVG 的原始颜色 if (icon.includes(" { btn.style.boxShadow = `0 6px 15px rgba(var(--primary-rgb), 0.3)`; }); btn.addEventListener("mouseleave", () => { btn.style.boxShadow = "0 4px 10px rgba(0,0,0,0.1)"; }); }, _makeDraggable(panel) { const header = panel.querySelector(".boss-header, .boss-chat-header"); if (!header) return; header.style.cursor = "move"; let isDragging = false; let startX = 0, startY = 0; let initialX = panel.offsetLeft, initialY = panel.offsetTop; header.addEventListener("mousedown", (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; initialX = panel.offsetLeft; initialY = panel.offsetTop; panel.style.transition = "none"; panel.style.zIndex = "2147483647"; }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; const viewportMargin = 12; const maxLeft = Math.max( viewportMargin, window.innerWidth - panel.offsetWidth - viewportMargin ); const maxTop = Math.max( viewportMargin, window.innerHeight - panel.offsetHeight - viewportMargin ); const nextLeft = Math.min( Math.max(viewportMargin, initialX + dx), maxLeft ); const nextTop = Math.min( Math.max(viewportMargin, initialY + dy), maxTop ); panel.style.left = `${nextLeft}px`; panel.style.top = `${nextTop}px`; panel.style.right = "auto"; }); document.addEventListener("mouseup", () => { if (isDragging) { isDragging = false; panel.style.transition = "all 0.3s ease"; panel.style.zIndex = "2147483646"; } }); }, createMiniIcon() { elements.miniIcon = document.createElement("div"); elements.miniIcon.style.cssText = ` width: ${CONFIG.MINI_ICON_SIZE || 48}px; height: ${CONFIG.MINI_ICON_SIZE || 48}px; position: fixed; bottom: 40px; left: 40px; background: var(--primary-color); border-radius: 50%; box-shadow: 0 6px 16px rgba(var(--primary-rgb), 0.4); cursor: pointer; display: none; justify-content: center; align-items: center; color: #fff; z-index: 2147483647; transition: all 0.3s ease; overflow: hidden; `; const customSvg = ` `; elements.miniIcon.innerHTML = customSvg; elements.miniIcon.addEventListener("mouseenter", () => { elements.miniIcon.style.transform = "scale(1.1)"; elements.miniIcon.style.boxShadow = `0 8px 20px rgba(var(--primary-rgb), 0.5)`; }); elements.miniIcon.addEventListener("mouseleave", () => { elements.miniIcon.style.transform = "scale(1)"; elements.miniIcon.style.boxShadow = `0 6px 16px rgba(var(--primary-rgb), 0.4)`; }); elements.miniIcon.addEventListener("click", () => { state.isMinimized = false; elements.panel.style.transform = "translateY(0)"; elements.miniIcon.style.display = "none"; }); document.body.appendChild(elements.miniIcon); }, _hexToRgb(hex) { hex = hex.replace("#", ""); const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); return `${r}, ${g}, ${b}`; }, }; const settings = { useAutoSendResume: JSON.parse( localStorage.getItem("useAutoSendResume") || "false" ), actionDelays: { click: parseInt(localStorage.getItem("clickDelay") || "130"), }, ai: { role: localStorage.getItem("aiRole") || "你是一个正在BOSS直聘上与HR聊天的求职者。回复要求:简短口语化(30字内),像朋友微信聊天;有经验说经验,没经验说能力;工资说范围、到岗给时间、离职说成长;不说您好、不模板化、不写小作文。", }, autoReply: JSON.parse(localStorage.getItem("autoReply") || "false"), useAutoSendImageResume: JSON.parse( localStorage.getItem("useAutoSendImageResume") || "false" ), imageResumeData: localStorage.getItem("imageResumeData") || null, communicationMode: localStorage.getItem("communicationMode") || "new-only", // 批量投递默认走基础规则;AI 匹配必须由用户显式开启。 deliveryMode: localStorage.getItem("deliveryMode") === "ai-match" ? "ai-match" : "basic", aiMatchThreshold: Math.min( 95, Math.max(50, parseInt(localStorage.getItem("aiMatchThreshold") || "70", 10) || 70) ), recruiterActivityStatus: JSON.parse( localStorage.getItem("recruiterActivityStatus") || '["不限"]' ), excludeHeadhunters: JSON.parse( localStorage.getItem("excludeHeadhunters") || "false" ), }; // 建议回复模式:不自动发送,仅填入输入框供审核 if (settings.communicationMode === 'suggested-reply') { console.log('[Boss扩展] 建议回复模式已激活:AI将生成回复建议填入输入框'); } function saveSettings() { localStorage.setItem( "useAutoSendResume", settings.useAutoSendResume.toString() ); localStorage.setItem("clickDelay", settings.actionDelays.click.toString()); localStorage.setItem("aiRole", settings.ai.role); localStorage.setItem("autoReply", settings.autoReply.toString()); localStorage.setItem( "useAutoSendImageResume", settings.useAutoSendImageResume.toString() ); if (settings.imageResumes) { localStorage.setItem( "imageResumes", JSON.stringify(settings.imageResumes) ); } if (settings.imageResumeData) { localStorage.setItem("imageResumeData", settings.imageResumeData); } else { localStorage.removeItem("imageResumeData"); } localStorage.setItem( "recruiterActivityStatus", JSON.stringify(settings.recruiterActivityStatus) ); localStorage.setItem( "excludeHeadhunters", settings.excludeHeadhunters.toString() ); // 保存新增设置 localStorage.setItem("communicationMode", settings.communicationMode); localStorage.setItem("deliveryMode", settings.deliveryMode === "ai-match" ? "ai-match" : "basic"); localStorage.setItem( "aiMatchThreshold", String(Math.min(95, Math.max(50, parseInt(settings.aiMatchThreshold, 10) || 70))) ); if (state.settings) { Object.assign(state.settings, settings); } } function createSettingsDialog() { const dialog = document.createElement("div"); dialog.id = "boss-settings-dialog"; dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: min(620px, calc(100vw - 24px)); max-width: calc(100vw - 24px); height: min(82vh, 760px); max-height: calc(100vh - 24px); background: #ffffff; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); z-index: 999999; display: none; flex-direction: column; font-family: 'Segoe UI', sans-serif; overflow: hidden; transition: all 0.3s ease; `; dialog.innerHTML += ` `; const dialogHeader = createDialogHeader("求职助手 · 设置"); const dialogContent = document.createElement("div"); dialogContent.style.cssText = ` padding: 14px 16px 16px; flex: 1; overflow-y: auto; scrollbar-width: thin; scrollbar-color: rgba(0, 123, 255, 0.5) rgba(0, 0, 0, 0.05); `; dialogContent.innerHTML += ` `; const tabsContainer = document.createElement("div"); tabsContainer.style.cssText = ` display: flex; border-bottom: 1px solid rgba(0, 123, 255, 0.2); margin-bottom: 14px; `; const aiTab = document.createElement("button"); aiTab.textContent = "聊天设置"; aiTab.className = "settings-tab active"; aiTab.style.cssText = ` padding: 9px 15px; background: rgba(0, 123, 255, 0.9); color: white; border: none; border-radius: 8px 8px 0 0; font-weight: 500; cursor: pointer; transition: all 0.2s ease; margin-right: 5px; `; const advancedTab = document.createElement("button"); advancedTab.textContent = "高级设置"; advancedTab.className = "settings-tab"; advancedTab.style.cssText = ` padding: 9px 15px; background: rgba(0, 0, 0, 0.05); color: #333; border: none; border-radius: 8px 8px 0 0; font-weight: 500; cursor: pointer; transition: all 0.2s ease; margin-right: 5px; `; tabsContainer.append(aiTab, advancedTab); const aiSettingsPanel = document.createElement("div"); aiSettingsPanel.id = "ai-settings-panel"; const roleSettingResult = createSettingItem( "AI角色定位", "定义AI在对话中的角色和语气特点", () => document.getElementById("ai-role-input") ); const roleSetting = roleSettingResult.settingItem; const roleInput = document.createElement("textarea"); roleInput.id = "ai-role-input"; roleInput.rows = 4; roleInput.style.cssText = ` width: 100%; min-height: 96px; box-sizing: border-box; padding: 10px 12px; border-radius: 8px; border: 1px solid #d1d5db; resize: vertical; font-size: 14px; transition: all 0.2s ease; margin-top: 10px; opacity: 1; pointer-events: auto; `; addFocusBlurEffects(roleInput); roleSetting.append(roleInput); aiSettingsPanel.append(roleSetting); // 简历上传和分析设置 const resumeUploadSettingResult = createSettingItem( "简历上传与 AI 分析", "上传或粘贴简历文本,用于 AI 分析与对话辅助。", () => document.getElementById("resume-upload-container") ); const resumeUploadSetting = resumeUploadSettingResult.settingItem; const resumeUploadContainer = document.createElement("div"); resumeUploadContainer.id = "resume-upload-container"; resumeUploadContainer.style.cssText = ` width: 100%; margin-top: 8px; `; // 文件上传区域 const fileUploadArea = document.createElement("div"); fileUploadArea.className = "resume-file-upload"; fileUploadArea.style.cssText = ` display: grid !important; grid-template-columns: auto minmax(0, 1fr) auto !important; align-items: center !important; gap: 12px; min-height: 76px !important; height: auto !important; box-sizing: border-box !important; border: 1.5px dashed #93c5fd; border-radius: 10px; padding: 10px 12px; text-align: left; background: #f8fbff; cursor: pointer; transition: all 0.2s ease; `; fileUploadArea.addEventListener("mouseenter", () => { fileUploadArea.style.borderColor = "#2563eb"; fileUploadArea.style.background = "#eff6ff"; }); fileUploadArea.addEventListener("mouseleave", () => { fileUploadArea.style.borderColor = "#93c5fd"; fileUploadArea.style.background = "#f8fbff"; }); // 隐藏的文件输入 const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = ".pdf,.doc,.docx,.txt"; fileInput.style.display = "none"; // 上传图标和文字 const uploadIcon = document.createElement("div"); uploadIcon.innerHTML = APP_ICONS.file; uploadIcon.style.cssText = ` width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; flex: 0 0 40px; color: #2563eb; background: #dbeafe; border-radius: 10px; `; const uploadText = document.createElement("div"); uploadText.textContent = "选择简历文件"; uploadText.style.cssText = ` font-size: 14px; font-weight: 700; color: #1f2937; margin-bottom: 3px; `; const uploadSubText = document.createElement("div"); uploadSubText.textContent = "PDF · Word · TXT,最大 10MB"; uploadSubText.style.cssText = ` font-size: 12px; color: #64748b; `; const resumeFileNameDisplay = document.createElement("div"); resumeFileNameDisplay.id = "resume-file-name"; resumeFileNameDisplay.style.cssText = ` margin-top: 3px; font-size: 12px; color: #2563eb; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; const uploadCopy = document.createElement("div"); uploadCopy.className = "resume-file-copy"; uploadCopy.style.cssText = "min-width:0;"; uploadCopy.append(uploadText, uploadSubText, resumeFileNameDisplay); const uploadAction = document.createElement("span"); uploadAction.textContent = "选择文件"; uploadAction.className = "resume-file-action"; uploadAction.style.cssText = "padding:7px 10px; border-radius:7px; background:#2563eb; color:#fff; font-size:12px; font-weight:700; white-space:nowrap;"; fileUploadArea.append(uploadIcon, uploadCopy, uploadAction); // 点击上传区域触发文件选择 fileUploadArea.addEventListener("click", () => { fileInput.click(); }); // 文件选择处理 fileInput.addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; // 检查文件类型 const validTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain']; const validExtensions = ['.pdf', '.doc', '.docx', '.txt']; const fileExtension = '.' + file.name.split('.').pop().toLowerCase(); if (!validTypes.includes(file.type) && !validExtensions.includes(fileExtension)) { showNotification("不支持的文件格式,请上传 PDF、Word 或 TXT 文件", "error"); return; } // 检查文件大小 (最大10MB) if (file.size > 10 * 1024 * 1024) { showNotification("文件太大,请上传小于 10MB 的文件", "error"); return; } resumeFileNameDisplay.textContent = `已选择: ${file.name}`; try { showNotification("正在读取文件内容...", "info"); const result = await Core.readResumeFile(file); if (result.success) { // 保存简历文本(使用安全存储) state.settings.resumeText = result.text; const saveResult = setLargeItem("resumeText", result.text); // 显示在文本框中(可编辑) resumeTextArea.value = result.text; if (saveResult === 'truncated') { showNotification(`文件读取成功!共 ${result.text.length} 字符。(内容已截断以符合存储限制)`, "warning"); } else if (saveResult === false) { showNotification(`文件读取成功!共 ${result.text.length} 字符。(无法保存到本地存储,但当前会话可用)`, "warning"); } else { showNotification(`文件读取成功!共 ${result.text.length} 字符。点击 AI分析简历 进行分析`, "success"); } } else { // 提取失败,显示部分内容和提示 if (result.text && result.text.length > 0) { resumeTextArea.value = result.text + "\n\n" + result.message; showNotification("文件内容提取不完整,请检查文本框中的提示", "warning"); } else { resumeTextArea.value = result.message; showNotification(result.message, "error"); } } } catch (error) { showNotification("读取文件失败: " + error.message, "error"); } }); // 简历文本编辑区域(读取文件后可编辑) const resumeTextLabel = document.createElement("div"); resumeTextLabel.textContent = "简历内容(可编辑):"; resumeTextLabel.style.cssText = ` font-size: 14px; font-weight: 600; color: #374151; margin-top: 12px; margin-bottom: 7px; `; // 添加提示说明 const pasteHint = document.createElement("div"); pasteHint.innerHTML = `
${APP_ICONS.info}提示:如果文件上传失败,请直接打开简历文件, Ctrl+A 全选后 Ctrl+C 复制,然后粘贴到下方文本框中。
`; const resumeTextArea = document.createElement("textarea"); resumeTextArea.id = "resume-text-input"; resumeTextArea.placeholder = "请上传简历文件,或在此直接粘贴简历内容...\n\n如果PDF/Word文件上传后显示乱码,请直接复制粘贴文本内容到这里"; resumeTextArea.value = state.settings.resumeText || ""; resumeTextArea.style.cssText = ` width: 100%; min-height: 140px; max-height: 280px; box-sizing: border-box; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; resize: vertical; font-family: inherit; line-height: 1.5; `; // 按钮容器 const resumeBtnContainer = document.createElement("div"); resumeBtnContainer.style.cssText = ` display: flex; gap: 10px; margin-top: 10px; `; // AI分析按钮 const analyzeBtn = document.createElement("button"); analyzeBtn.textContent = "AI 分析简历"; analyzeBtn.style.cssText = ` flex: 1; padding: 10px 16px; border-radius: 6px; border: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.3s ease; `; analyzeBtn.addEventListener("mouseenter", () => { analyzeBtn.style.transform = "translateY(-2px)"; analyzeBtn.style.boxShadow = "0 4px 12px rgba(102, 126, 234, 0.4)"; }); analyzeBtn.addEventListener("mouseleave", () => { analyzeBtn.style.transform = "translateY(0)"; analyzeBtn.style.boxShadow = "none"; }); analyzeBtn.addEventListener("click", async () => { const resumeText = resumeTextArea.value.trim(); if (!resumeText) { showNotification("请先上传简历文件或输入简历内容", "error"); return; } // 保存简历文本(使用安全存储) state.settings.resumeText = resumeText; const saveResult = setLargeItem("resumeText", resumeText); if (saveResult === 'truncated') { showNotification("简历内容已截断以符合存储限制", "warning"); } else if (saveResult === false) { showNotification("无法保存到本地存储,但当前会话可用", "warning"); } analyzeBtn.textContent = "正在分析简历…"; analyzeBtn.disabled = true; try { showNotification("正在分析简历,预计需要 5-15 秒...", "info"); const startTime = Date.now(); const analysis = await Core.analyzeResumeWithAI(resumeText); const duration = ((Date.now() - startTime) / 1000).toFixed(1); if (analysis) { state.settings.resumeAnalysis = analysis; setLargeItem("resumeAnalysis", analysis); analysisResultArea.value = analysis; showNotification(`简历分析完成!耗时 ${duration} 秒`, "success"); // 可选:仅根据简历生成一条首次沟通自我介绍 if (confirm("简历分析完成!是否根据简历生成一条自我介绍?")) { analyzeBtn.textContent = "生成自我介绍..."; const greetings = await Core.generateGreetingsFromResume(resumeText); if (greetings) { showNotification("已生成 1 条自我介绍,可在设置中微调", "success"); } } } } catch (error) { showNotification("分析失败: " + error.message, "error"); } finally { analyzeBtn.textContent = "AI 分析简历"; analyzeBtn.disabled = false; } }); // 保存按钮 const saveResumeBtn = document.createElement("button"); saveResumeBtn.textContent = "保存简历"; saveResumeBtn.style.cssText = ` padding: 10px 16px; border-radius: 6px; border: 1px solid #10b981; background: rgba(16, 185, 129, 0.1); color: #10b981; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.3s ease; `; saveResumeBtn.addEventListener("mouseenter", () => { saveResumeBtn.style.background = "rgba(16, 185, 129, 0.2)"; }); saveResumeBtn.addEventListener("mouseleave", () => { saveResumeBtn.style.background = "rgba(16, 185, 129, 0.1)"; }); saveResumeBtn.addEventListener("click", () => { state.settings.resumeText = resumeTextArea.value; const saveResult = setLargeItem("resumeText", resumeTextArea.value); if (saveResult === 'truncated') { showNotification("简历已保存(内容已截断以符合存储限制)", "warning"); } else if (saveResult === false) { showNotification("无法保存到本地存储,但当前会话可用", "warning"); } else { showNotification("简历已保存!", "success"); } }); resumeBtnContainer.append(analyzeBtn, saveResumeBtn); // 分析结果显示区域 const analysisLabel = document.createElement("div"); analysisLabel.textContent = "AI分析结果(用于智能回复):"; analysisLabel.style.cssText = ` font-size: 14px; font-weight: 600; color: #374151; margin-top: 12px; margin-bottom: 7px; `; const analysisResultArea = document.createElement("textarea"); analysisResultArea.id = "resume-analysis-result"; analysisResultArea.placeholder = "AI分析结果将显示在这里..."; analysisResultArea.value = state.settings.resumeAnalysis || ""; analysisResultArea.style.cssText = ` width: 100%; min-height: 84px; max-height: 220px; box-sizing: border-box; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 13px; resize: vertical; font-family: inherit; line-height: 1.5; background: #f9fafb; `; resumeUploadContainer.append( fileUploadArea, fileInput, pasteHint, resumeTextLabel, resumeTextArea, resumeBtnContainer, analysisLabel, analysisResultArea ); resumeUploadSetting.append(resumeUploadContainer); aiSettingsPanel.append(resumeUploadSetting); // 候选人自身工作经验设置 const candidateExperienceSettingResult = createSettingItem( "我的工作经验", "选择您的工作经验年限,用于匹配适合的职位", () => document.getElementById("candidate-experience-select .select-header") ); const candidateExperienceSetting = candidateExperienceSettingResult.settingItem; const candidateExperienceSelect = document.createElement("div"); candidateExperienceSelect.id = "candidate-experience-select"; candidateExperienceSelect.className = "custom-select"; candidateExperienceSelect.style.cssText = ` position: relative; width: 100%; margin-top: 10px; `; const candidateExpHeader = document.createElement("div"); candidateExpHeader.className = "select-header"; candidateExpHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-radius: 8px; border: 1px solid #e2e8f0; background: white; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); min-height: 44px; `; const candidateExpDisplay = document.createElement("div"); candidateExpDisplay.className = "select-value"; candidateExpDisplay.style.cssText = ` flex: 1; text-align: left; color: #334155; font-size: 14px; `; function getCandidateExpDisplayText() { const exp = settings.candidateExperience || ""; if (!exp) return "请选择您的工作经验"; const expMap = { "fresher": "应届生", "1year": "1年以内", "1-3year": "1-3年", "3-5year": "3-5年", "5-10year": "5-10年", "10+year": "10年以上" }; return expMap[exp] || exp; } candidateExpDisplay.textContent = getCandidateExpDisplayText(); const candidateExpIcon = document.createElement("div"); candidateExpIcon.className = "select-icon"; candidateExpIcon.innerHTML = "▼"; candidateExpIcon.style.cssText = ` margin-left: 10px; color: #64748b; transition: transform 0.2s ease; `; candidateExpHeader.append(candidateExpDisplay, candidateExpIcon); const candidateExpOptions = document.createElement("div"); candidateExpOptions.className = "select-options"; candidateExpOptions.style.cssText = ` position: absolute; top: calc(100% + 6px); left: 0; right: 0; max-height: 280px; overflow-y: auto; border-radius: 8px; border: 1px solid #e2e8f0; background: white; z-index: 100; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: none; transition: all 0.2s ease; `; const candidateExpOptionsList = [ { value: "", text: "请选择工作经验" }, { value: "fresher", text: "应届生(无工作经验)" }, { value: "1year", text: "1年以内" }, { value: "1-3year", text: "1-3年" }, { value: "3-5year", text: "3-5年" }, { value: "5-10year", text: "5-10年" }, { value: "10+year", text: "10年以上" } ]; candidateExpOptionsList.forEach((option) => { const expOption = document.createElement("div"); const isSelected = settings.candidateExperience === option.value; expOption.className = "select-option" + (isSelected ? " selected" : ""); expOption.dataset.value = option.value; expOption.style.cssText = ` padding: 12px 16px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; font-size: 14px; color: #334155; `; const checkIcon = document.createElement("span"); checkIcon.className = "check-icon"; checkIcon.innerHTML = APP_ICONS.check; checkIcon.style.cssText = ` margin-right: 8px; color: rgba(0, 123, 255, 0.9); font-weight: bold; display: ${isSelected ? "inline-flex" : "none"}; `; const textSpan = document.createElement("span"); textSpan.textContent = option.text; expOption.append(checkIcon, textSpan); expOption.addEventListener("click", (e) => { e.stopPropagation(); settings.candidateExperience = option.value; candidateExpDisplay.textContent = getCandidateExpDisplayText(); candidateExpOptions.style.display = "none"; candidateExpIcon.style.transform = "rotate(0)"; candidateExpOptions.querySelectorAll(".select-option").forEach((opt) => { const optCheck = opt.querySelector(".check-icon"); optCheck.style.display = opt.dataset.value === option.value ? "inline" : "none"; opt.classList.toggle("selected", opt.dataset.value === option.value); }); StatePersistence.saveState(); showNotification("工作经验已保存", "success"); }); candidateExpOptions.appendChild(expOption); }); candidateExpHeader.addEventListener("click", () => { candidateExpOptions.style.display = candidateExpOptions.style.display === "block" ? "none" : "block"; candidateExpIcon.style.transform = candidateExpOptions.style.display === "block" ? "rotate(180deg)" : "rotate(0)"; }); document.addEventListener("click", (e) => { if (!candidateExperienceSelect.contains(e.target)) { candidateExpOptions.style.display = "none"; candidateExpIcon.style.transform = "rotate(0)"; } }); candidateExpHeader.addEventListener("mouseenter", () => { candidateExpHeader.style.borderColor = "rgba(0, 123, 255, 0.5)"; candidateExpHeader.style.boxShadow = "0 0 0 3px rgba(0, 123, 255, 0.1)"; }); candidateExpHeader.addEventListener("mouseleave", () => { candidateExpHeader.style.borderColor = "#e2e8f0"; candidateExpHeader.style.boxShadow = "0 1px 2px rgba(0, 0, 0, 0.05)"; }); candidateExperienceSelect.append(candidateExpHeader, candidateExpOptions); candidateExperienceSetting.append(candidateExperienceSelect); aiSettingsPanel.append(candidateExperienceSetting); // 单条自我介绍:仅用于首次沟通,可在 AI 生成后微调 const greetingsSettingResult = createSettingItem( "AI 自我介绍", "分析简历后会仅根据简历生成 1 条简洁礼貌的首次沟通介绍。", () => document.getElementById("greetings-list") ); const greetingsSetting = greetingsSettingResult.settingItem; const greetingsContainer = document.createElement("div"); greetingsContainer.id = "greetings-container"; greetingsContainer.style.cssText = ` width: 100%; margin-top: 10px; `; const greetingsList = document.createElement("div"); greetingsList.id = "greetings-list"; greetingsList.style.cssText = ` border: 1px solid #dbe5f0; border-radius: 10px; padding: 12px; background: #f8fafc; `; greetingsContainer.append(greetingsList); greetingsSetting.append(greetingsContainer); aiSettingsPanel.append(greetingsSetting); const advancedSettingsPanel = document.createElement("div"); advancedSettingsPanel.id = "advanced-settings-panel"; advancedSettingsPanel.style.display = "none"; const autoReplySettingResult = createSettingItem( "Ai回复模式", "开启后Ai将自动回复消息", () => document.querySelector("#toggle-auto-reply-mode input") ); const autoReplySetting = autoReplySettingResult.settingItem; const autoReplyDescriptionContainer = autoReplySettingResult.descriptionContainer; const autoReplyToggle = createToggleSwitch( "auto-reply-mode", settings.autoReply, (checked) => { settings.autoReply = checked; } ); autoReplyDescriptionContainer.append(autoReplyToggle); const autoSendResumeSettingResult = createSettingItem( "自动发送附件简历", "开启后系统将自动发送附件简历给HR", () => document.querySelector("#toggle-auto-send-resume input") ); const autoSendResumeSetting = autoSendResumeSettingResult.settingItem; const autoSendResumeDescriptionContainer = autoSendResumeSettingResult.descriptionContainer; const autoSendResumeToggle = createToggleSwitch( "auto-send-resume", settings.useAutoSendResume, (checked) => { settings.useAutoSendResume = checked; } ); autoSendResumeDescriptionContainer.append(autoSendResumeToggle); const excludeHeadhuntersSettingResult = createSettingItem( "投递时排除猎头", "开启后将不会向猎头职位自动投递简历", () => document.querySelector("#toggle-exclude-headhunters input") ); const excludeHeadhuntersSetting = excludeHeadhuntersSettingResult.settingItem; const excludeHeadhuntersDescriptionContainer = excludeHeadhuntersSettingResult.descriptionContainer; const excludeHeadhuntersToggle = createToggleSwitch( "exclude-headhunters", settings.excludeHeadhunters, (checked) => { settings.excludeHeadhunters = checked; } ); excludeHeadhuntersDescriptionContainer.append(excludeHeadhuntersToggle); const imageResumeSettingResult = createSettingItem( "发送图片简历", "首次沟通发送图片简历(需先选择JPG格式图片)", () => document.querySelector("#toggle-auto-send-image-resume input") ); const imageResumeSetting = imageResumeSettingResult.settingItem; const imageResumeDescriptionContainer = imageResumeSettingResult.descriptionContainer; if (!state.settings.imageResumes) { state.settings.imageResumes = []; } const fileInputContainer = document.createElement("div"); fileInputContainer.style.cssText = ` display: flex; flex-direction: column; gap: 10px; width: 100%; margin-top: 10px; `; const addResumeBtn = document.createElement("button"); addResumeBtn.id = "add-image-resume-btn"; addResumeBtn.textContent = "添加图片简历"; addResumeBtn.style.cssText = ` padding: 8px 16px; border-radius: 6px; border: 1px solid rgba(0, 123, 255, 0.7); background: rgba(0, 123, 255, 0.1); color: rgba(0, 123, 255, 0.9); cursor: pointer; font-size: 14px; transition: all 0.2s ease; align-self: flex-start; white-space: nowrap; `; const fileNameDisplay = document.createElement("div"); fileNameDisplay.id = "image-resume-filename"; fileNameDisplay.style.cssText = ` flex: 1; padding: 8px; border-radius: 6px; border: 1px solid #d1d5db; background: #f8fafc; color: #334155; font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; `; const resumeCount = state.settings.imageResumes ? state.settings.imageResumes.length : 0; fileNameDisplay.textContent = resumeCount > 0 ? `已上传 ${resumeCount} 个简历` : "未选择文件"; const autoSendImageResumeToggle = (() => { const hasImageResumes = state.settings.imageResumes && state.settings.imageResumes.length > 0; const isValidState = hasImageResumes && settings.useAutoSendImageResume; if (!hasImageResumes) settings.useAutoSendImageResume = false; return createToggleSwitch( "auto-send-image-resume", isValidState, (checked) => { if ( checked && (!state.settings.imageResumes || state.settings.imageResumes.length === 0) ) { showNotification("请先选择图片文件", "error"); const slider = document.querySelector( "#toggle-auto-send-image-resume .toggle-slider" ); const container = document.querySelector( "#toggle-auto-send-image-resume .toggle-switch" ); container.style.backgroundColor = "#e5e7eb"; slider.style.transform = "translateX(0)"; document.querySelector( "#toggle-auto-send-image-resume input" ).checked = false; } settings.useAutoSendImageResume = checked; return true; }, true ); })(); const hiddenFileInput = document.createElement("input"); hiddenFileInput.id = "image-resume-input"; hiddenFileInput.type = "file"; hiddenFileInput.accept = ".jpg,.jpeg"; hiddenFileInput.style.display = "none"; const uploadedResumesContainer = document.createElement("div"); uploadedResumesContainer.id = "uploaded-resumes-container"; uploadedResumesContainer.style.cssText = ` display: flex; flex-direction: column; gap: 8px; width: 100%; `; function renderResumeItem(index, resume) { const resumeItem = document.createElement("div"); resumeItem.style.cssText = ` display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-radius: 6px; background: rgba(0, 0, 0, 0.05); font-size: 14px; `; const fileNameSpan = document.createElement("span"); fileNameSpan.textContent = resume.path; fileNameSpan.style.cssText = ` flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 8px; `; const deleteBtn = document.createElement("button"); deleteBtn.textContent = "删除"; deleteBtn.style.cssText = ` padding: 4px 12px; border-radius: 4px; border: 1px solid rgba(255, 70, 70, 0.7); background: rgba(255, 70, 70, 0.1); color: rgba(255, 70, 70, 0.9); cursor: pointer; font-size: 12px; `; deleteBtn.addEventListener("click", () => { state.settings.imageResumes.splice(index, 1); resumeItem.remove(); if (state.settings.imageResumes.length === 0) { state.settings.useAutoSendImageResume = false; const toggleInput = document.querySelector( "#toggle-auto-send-image-resume input" ); if (toggleInput) { toggleInput.checked = false; toggleInput.dispatchEvent(new Event("change")); } } if ( typeof StatePersistence !== "undefined" && StatePersistence.saveState ) { StatePersistence.saveState(); } }); resumeItem.appendChild(fileNameSpan); resumeItem.appendChild(deleteBtn); return resumeItem; } if (state.settings.imageResumes && state.settings.imageResumes.length > 0) { state.settings.imageResumes.forEach((resume, index) => { const resumeItem = renderResumeItem(index, resume); uploadedResumesContainer.appendChild(resumeItem); }); } addResumeBtn.addEventListener("click", () => { if (state.settings.imageResumes.length >= 5) { if (typeof showNotification !== "undefined") { showNotification("免费版最多添加5个图片简历", "info"); } else { alert("免费版最多添加5个图片简历"); } } else { hiddenFileInput.click(); } }); hiddenFileInput.addEventListener("change", (e) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; const fileName = file.name.toLowerCase(); if (!fileName.endsWith('.jpg') && !fileName.endsWith('.jpeg')) { if (typeof showNotification !== "undefined") { showNotification("仅支持JPG格式的图片文件", "error"); } else { alert("仅支持JPG格式的图片文件"); } hiddenFileInput.value = ""; return; } const isDuplicate = state.settings.imageResumes.some( (resume) => resume.path === file.name ); if (isDuplicate) { if (typeof showNotification !== "undefined") { showNotification("该文件名已存在", "error"); } else { alert("该文件名已存在"); } return; } const reader = new FileReader(); reader.onload = function (event) { const newResume = { path: file.name, data: event.target.result, }; state.settings.imageResumes.push(newResume); const index = state.settings.imageResumes.length - 1; const resumeItem = renderResumeItem(index, newResume); uploadedResumesContainer.appendChild(resumeItem); if (!state.settings.useAutoSendImageResume) { state.settings.useAutoSendImageResume = true; const toggleInput = document.querySelector( "#toggle-auto-send-image-resume input" ); if (toggleInput) { toggleInput.checked = true; toggleInput.dispatchEvent(new Event("change")); } } if ( typeof StatePersistence !== "undefined" && StatePersistence.saveState ) { StatePersistence.saveState(); } }; reader.readAsDataURL(file); } }); fileInputContainer.append( addResumeBtn, uploadedResumesContainer, hiddenFileInput ); imageResumeDescriptionContainer.append(autoSendImageResumeToggle); imageResumeSetting.append(fileInputContainer); const recruiterStatusSettingResult = createSettingItem( "投递招聘者状态(多选)", "筛选活跃状态符合要求的招聘者进行投递", () => document.querySelector("#recruiter-status-select .select-header") ); const recruiterStatusSetting = recruiterStatusSettingResult.settingItem; const statusSelect = document.createElement("div"); statusSelect.id = "recruiter-status-select"; statusSelect.className = "custom-select"; statusSelect.style.cssText = ` position: relative; width: 100%; margin-top: 10px; `; const statusHeader = document.createElement("div"); statusHeader.className = "select-header"; statusHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-radius: 8px; border: 1px solid #e2e8f0; background: white; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); min-height: 44px; `; const statusDisplay = document.createElement("div"); statusDisplay.className = "select-value"; statusDisplay.style.cssText = ` flex: 1; text-align: left; color: #334155; font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; `; statusDisplay.textContent = getStatusDisplayText(); const statusIcon = document.createElement("div"); statusIcon.className = "select-icon"; statusIcon.innerHTML = "▼"; statusIcon.style.cssText = ` margin-left: 10px; color: #64748b; transition: transform 0.2s ease; `; const statusClear = document.createElement("button"); statusClear.className = "select-clear"; statusClear.innerHTML = "×"; statusClear.style.cssText = ` background: none; border: none; color: #94a3b8; cursor: pointer; font-size: 16px; margin-left: 8px; display: none; transition: color 0.2s ease; `; statusHeader.append(statusDisplay, statusClear, statusIcon); const statusOptions = document.createElement("div"); statusOptions.className = "select-options"; statusOptions.style.cssText = ` position: absolute; top: calc(100% + 6px); left: 0; right: 0; max-height: 240px; overflow-y: auto; border-radius: 8px; border: 1px solid #e2e8f0; background: white; z-index: 100; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: none; transition: all 0.2s ease; scrollbar-width: thin; scrollbar-color: #cbd5e1 #f1f5f9; `; statusOptions.innerHTML += ` `; const statusOptionsList = [ { value: "不限", text: "不限" }, { value: "在线", text: "在线" }, { value: "刚刚活跃", text: "刚刚活跃" }, { value: "今日活跃", text: "今日活跃" }, { value: "3日内活跃", text: "3日内活跃" }, { value: "本周活跃", text: "本周活跃" }, { value: "本月活跃", text: "本月活跃" }, { value: "半年前活跃", text: "半年前活跃" }, ]; statusOptionsList.forEach((option) => { const statusOption = document.createElement("div"); statusOption.className = "select-option" + (settings.recruiterActivityStatus && Array.isArray(settings.recruiterActivityStatus) && settings.recruiterActivityStatus.includes(option.value) ? " selected" : ""); statusOption.dataset.value = option.value; statusOption.style.cssText = ` padding: 12px 16px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; font-size: 14px; color: #334155; `; const checkIcon = document.createElement("span"); checkIcon.className = "check-icon"; checkIcon.innerHTML = APP_ICONS.check; checkIcon.style.cssText = ` margin-right: 8px; color: rgba(0, 123, 255, 0.9); font-weight: bold; display: ${settings.recruiterActivityStatus && Array.isArray(settings.recruiterActivityStatus) && settings.recruiterActivityStatus.includes(option.value) ? "inline" : "none" }; `; const textSpan = document.createElement("span"); textSpan.textContent = option.text; statusOption.append(checkIcon, textSpan); statusOption.addEventListener("click", (e) => { e.stopPropagation(); toggleStatusOption(option.value); }); statusOptions.appendChild(statusOption); }); statusHeader.addEventListener("click", () => { statusOptions.style.display = statusOptions.style.display === "block" ? "none" : "block"; statusIcon.style.transform = statusOptions.style.display === "block" ? "rotate(180deg)" : "rotate(0)"; }); statusClear.addEventListener("click", (e) => { e.stopPropagation(); settings.recruiterActivityStatus = []; updateStatusOptions(); }); document.addEventListener("click", (e) => { if (!statusSelect.contains(e.target)) { statusOptions.style.display = "none"; statusIcon.style.transform = "rotate(0)"; } }); statusHeader.addEventListener("mouseenter", () => { statusHeader.style.borderColor = "rgba(0, 123, 255, 0.5)"; statusHeader.style.boxShadow = "0 0 0 3px rgba(0, 123, 255, 0.1)"; }); statusHeader.addEventListener("mouseleave", () => { if (!statusHeader.contains(document.activeElement)) { statusHeader.style.borderColor = "#e2e8f0"; statusHeader.style.boxShadow = "0 1px 2px rgba(0, 0, 0, 0.05)"; } }); statusHeader.addEventListener("focus", () => { statusHeader.style.borderColor = "rgba(0, 123, 255, 0.7)"; statusHeader.style.boxShadow = "0 0 0 3px rgba(0, 123, 255, 0.2)"; }); statusHeader.addEventListener("blur", () => { statusHeader.style.borderColor = "#e2e8f0"; statusHeader.style.boxShadow = "0 1px 2px rgba(0, 0, 0, 0.05)"; }); statusSelect.append(statusHeader, statusOptions); recruiterStatusSetting.append(statusSelect); advancedSettingsPanel.append( autoReplySetting, autoSendResumeSetting, excludeHeadhuntersSetting, imageResumeSetting, recruiterStatusSetting ); aiTab.addEventListener("click", () => { setActiveTab(aiTab, aiSettingsPanel); }); advancedTab.addEventListener("click", () => { setActiveTab(advancedTab, advancedSettingsPanel); }); const dialogFooter = document.createElement("div"); dialogFooter.style.cssText = ` padding: 15px 20px; border-top: 1px solid #e5e7eb; display: flex; justify-content: flex-end; gap: 10px; background: rgba(0, 0, 0, 0.03); `; const cancelBtn = createTextButton("取消", "#e5e7eb", () => { dialog.style.display = "none"; }); const saveBtn = createTextButton( "保存设置", "rgba(0, 123, 255, 0.9)", () => { try { const aiRoleInput = document.getElementById("ai-role-input"); settings.ai.role = aiRoleInput ? aiRoleInput.value : ""; saveSettings(); showNotification("设置已保存"); dialog.style.display = "none"; } catch (error) { showNotification("保存失败: " + error.message, "error"); console.error("保存设置失败:", error); } } ); dialogFooter.append(cancelBtn, saveBtn); dialogContent.append( tabsContainer, aiSettingsPanel, advancedSettingsPanel ); dialog.append(dialogHeader, dialogContent, dialogFooter); dialog.addEventListener("click", (e) => { if (e.target === dialog) { dialog.style.display = "none"; } }); return dialog; } function showSettingsDialog() { let dialog = document.getElementById("boss-settings-dialog"); if (!dialog) { dialog = createSettingsDialog(); document.body.appendChild(dialog); } dialog.style.display = "flex"; setTimeout(() => { dialog.classList.add("active"); setTimeout(loadSettingsIntoUI, 100); }, 10); } function toggleStatusOption(value) { if (value === "不限") { settings.recruiterActivityStatus = settings.recruiterActivityStatus.includes("不限") ? [] : ["不限"]; } else { if (settings.recruiterActivityStatus.includes("不限")) { settings.recruiterActivityStatus = [value]; } else { if (settings.recruiterActivityStatus.includes(value)) { settings.recruiterActivityStatus = settings.recruiterActivityStatus.filter((v) => v !== value); } else { settings.recruiterActivityStatus.push(value); } if (settings.recruiterActivityStatus.length === 0) { settings.recruiterActivityStatus = ["不限"]; } } } if (state.settings) { state.settings.recruiterActivityStatus = settings.recruiterActivityStatus; } updateStatusOptions(); } function updateStatusOptions() { const options = document.querySelectorAll( "#recruiter-status-select .select-option" ); options.forEach((option) => { const isSelected = settings.recruiterActivityStatus.includes( option.dataset.value ); option.className = "select-option" + (isSelected ? " selected" : ""); option.querySelector(".check-icon").style.display = isSelected ? "inline" : "none"; if (option.dataset.value === "不限") { if (isSelected) { options.forEach((opt) => { if (opt.dataset.value !== "不限") { opt.className = "select-option"; opt.querySelector(".check-icon").style.display = "none"; } }); } } else if (settings.recruiterActivityStatus.includes("不限")) { option.querySelector(".check-icon").style.display = "none"; option.className = "select-option"; } }); document.querySelector( "#recruiter-status-select .select-value" ).textContent = getStatusDisplayText(); document.querySelector( "#recruiter-status-select .select-clear" ).style.display = settings.recruiterActivityStatus.length > 0 && !settings.recruiterActivityStatus.includes("不限") ? "inline" : "none"; if (state.settings) { state.settings.recruiterActivityStatus = settings.recruiterActivityStatus; } } function getStatusDisplayText() { if (settings.recruiterActivityStatus.includes("不限")) { return "不限"; } if (settings.recruiterActivityStatus.length === 0) { return "请选择"; } if (settings.recruiterActivityStatus.length <= 2) { return settings.recruiterActivityStatus.join("、"); } return `${settings.recruiterActivityStatus[0]}、${settings.recruiterActivityStatus[1]}等${settings.recruiterActivityStatus.length}项`; } function loadSettingsIntoUI() { const aiRoleInput = document.getElementById("ai-role-input"); if (aiRoleInput) { aiRoleInput.value = settings.ai.role; } const autoReplyInput = document.querySelector( "#toggle-auto-reply-mode input" ); if (autoReplyInput) { autoReplyInput.checked = settings.autoReply; } const autoSendResumeInput = document.querySelector( "#toggle-auto-send-resume input" ); if (autoSendResumeInput) { autoSendResumeInput.checked = settings.useAutoSendResume; } const excludeHeadhuntersInput = document.querySelector( "#toggle-exclude-headhunters input" ); if (excludeHeadhuntersInput) { excludeHeadhuntersInput.checked = settings.excludeHeadhunters; } const autoSendImageResumeInput = document.querySelector( "#toggle-auto-send-image-resume input" ); if (autoSendImageResumeInput) { autoSendImageResumeInput.checked = settings.useAutoSendImageResume && settings.imageResumes && settings.imageResumes.length > 0; } const communicationModeSelector = document.querySelector( "#communication-mode-selector select" ); if (communicationModeSelector) { communicationModeSelector.value = settings.communicationMode; } if (elements.communicationIncludeInput) { elements.communicationIncludeInput.value = settings.communicationIncludeKeywords || ""; } updateStatusOptions(); } function createDialogHeader(title, dialogId = "boss-settings-dialog") { const header = document.createElement("div"); header.style.cssText = ` padding: 16px 20px; background: #4285f4; color: white; font-size: 18px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; position: relative; border-radius: 12px 12px 0 0; `; const titleElement = document.createElement("div"); titleElement.style.cssText = "display:flex; align-items:center; gap:8px; font-weight:700; min-width:0;"; const titleIcon = document.createElement("span"); titleIcon.innerHTML = APP_ICONS.settings; titleIcon.style.cssText = "display:flex; flex:0 0 auto;"; const titleText = document.createElement("span"); titleText.textContent = title; titleText.style.cssText = "overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"; titleElement.append(titleIcon, titleText); const closeBtn = document.createElement("button"); closeBtn.innerHTML = APP_ICONS.close; closeBtn.title = "关闭"; closeBtn.style.cssText = ` width: 28px; height: 28px; background: rgba(255, 255, 255, 0.2); color: white; border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: all 0.2s ease; border: none; font-size: 16px; font-weight: bold; `; closeBtn.addEventListener("mouseenter", () => { closeBtn.style.backgroundColor = "rgba(255, 255, 255, 0.3)"; closeBtn.style.transform = "scale(1.1)"; }); closeBtn.addEventListener("mouseleave", () => { closeBtn.style.backgroundColor = "rgba(255, 255, 255, 0.2)"; closeBtn.style.transform = "scale(1)"; }); closeBtn.addEventListener("click", () => { const dialog = document.getElementById(dialogId); if (dialog) { dialog.style.display = "none"; } }); header.append(titleElement, closeBtn); return header; } function showAIConfigDialog() { let dialog = document.getElementById("boss-ai-config-dialog"); if (!dialog) { dialog = createAIConfigDialog(); document.body.appendChild(dialog); } dialog.style.display = "flex"; setTimeout(() => { dialog.classList.add("active"); }, 10); } function createAIConfigDialog() { const dialog = document.createElement("div"); dialog.id = "boss-ai-config-dialog"; dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: clamp(360px, 90vw, 480px); max-height: 85vh; background: #ffffff; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); z-index: 999999; display: none; flex-direction: column; font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; overflow: hidden; transition: all 0.3s ease; `; dialog.innerHTML = `
${APP_ICONS.sparkles}AI 服务配置

支持主流 AI 平台、OpenAI 兼容接口及任意中转站

中转站一般选择「OpenAI 兼容」;如服务商要求不同协议或自定义鉴权头,也可在下方配置。

用于需要 x-api-key、渠道标识等额外鉴权的服务。填写 JSON 对象;已填写的 Authorization / x-api-key 不会被脚本覆盖。

`; setTimeout(() => { // 预设全部使用 OpenAI 兼容 Chat Completions 格式;自建服务和中转站可直接填写完整地址。 const aiPresets = { openai: { type: "openai", url: "https://api.openai.com/v1/chat/completions", model: "gpt-4o-mini" }, deepseek: { type: "openai", url: "https://api.deepseek.com/chat/completions", model: "deepseek-chat" }, siliconflow: { type: "openai", url: "https://api.siliconflow.cn/v1/chat/completions", model: "deepseek-ai/DeepSeek-V3" }, openrouter: { type: "openai", url: "https://openrouter.ai/api/v1/chat/completions", model: "openai/gpt-4o-mini" }, volcengine: { type: "openai", url: "https://ark.cn-beijing.volces.com/api/v3/chat/completions", model: "" }, anthropic: { type: "anthropic", url: "https://api.anthropic.com/v1/messages", model: "claude-3-5-haiku-latest" }, gemini: { type: "gemini", url: "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}", model: "gemini-2.0-flash" } }; // 加载已保存的配置 const apiKeyInput = document.getElementById("ai-api-key"); const apiUrlInput = document.getElementById("ai-api-url"); const modelInput = document.getElementById("ai-model"); const apiTypeInput = document.getElementById("ai-api-type"); const roleInput = document.getElementById("ai-role-config"); const customHeadersInput = document.getElementById("ai-custom-headers"); const statusDiv = document.getElementById("ai-config-status"); if (apiKeyInput) apiKeyInput.value = localStorage.getItem("aiApiKey") || ""; if (apiUrlInput) apiUrlInput.value = localStorage.getItem("aiApiUrl") || ""; if (modelInput) modelInput.value = localStorage.getItem("aiModel") || ""; if (apiTypeInput) apiTypeInput.value = localStorage.getItem("aiApiType") || "openai"; if (roleInput) roleInput.value = localStorage.getItem("aiRole") || "你是一个正在BOSS直聘上与HR聊天的求职者。回复要求:简短口语化(30字内),像朋友微信聊天;有经验说经验,没经验说能力;工资说范围、到岗给时间、离职说成长;不说您好、不模板化、不写小作文。"; if (customHeadersInput) customHeadersInput.value = localStorage.getItem("aiCustomHeaders") || ""; // 平台预设按钮点击事件 document.querySelectorAll(".ai-preset-btn").forEach(btn => { btn.addEventListener("click", () => { const preset = btn.dataset.preset; const config = aiPresets[preset]; if (config && apiUrlInput && modelInput) { apiUrlInput.value = config.url; modelInput.value = config.model; if (apiTypeInput) apiTypeInput.value = config.type || "openai"; if (statusDiv) statusDiv.textContent = `已选择:${btn.textContent}`; } }); }); // 测试连接按钮 const testBtn = document.getElementById("ai-test-btn"); if (testBtn) { testBtn.addEventListener("click", async () => { const apiKey = apiKeyInput?.value?.trim(); const apiUrl = apiUrlInput?.value?.trim(); const model = modelInput?.value?.trim(); const apiType = apiTypeInput?.value || "openai"; const customHeaders = customHeadersInput?.value?.trim() || ""; if (!apiUrl || !model) { if (statusDiv) { statusDiv.textContent = "请至少填写 API 地址和模型名称"; statusDiv.style.color = "#ea4335"; } return; } testBtn.disabled = true; testBtn.textContent = "测试中..."; if (statusDiv) statusDiv.textContent = "正在测试API连接..."; try { const result = await testAiConnection(apiKey, apiUrl, model, apiType, customHeaders); if (result.success) { if (statusDiv) { statusDiv.textContent = "连接成功:AI 回复:" + result.message; statusDiv.style.color = "#34a853"; } } else { if (statusDiv) { statusDiv.textContent = "连接失败:" + result.message; statusDiv.style.color = "#ea4335"; } } } catch (error) { if (statusDiv) { statusDiv.textContent = "测试出错:" + error.message; statusDiv.style.color = "#ea4335"; } } finally { testBtn.disabled = false; testBtn.textContent = "测试连接"; } }); } // 保存配置按钮 const saveBtn = document.getElementById("ai-save-btn"); if (saveBtn) { saveBtn.addEventListener("click", () => { const apiKey = apiKeyInput?.value?.trim(); const apiUrl = apiUrlInput?.value?.trim(); const model = modelInput?.value?.trim(); const apiType = apiTypeInput?.value || "openai"; const role = roleInput?.value?.trim(); const customHeaders = customHeadersInput?.value?.trim() || ""; try { parseAiCustomHeaders(customHeaders); } catch (error) { if (statusDiv) { statusDiv.textContent = "配置无效:" + error.message; statusDiv.style.color = "#ea4335"; } return; } localStorage.setItem("aiApiKey", apiKey || ""); localStorage.setItem("aiApiUrl", apiUrl || ""); localStorage.setItem("aiModel", model || ""); localStorage.setItem("aiApiType", apiType); localStorage.setItem("aiCustomHeaders", customHeaders); localStorage.setItem("aiRole", role || ""); // 更新状态 state.settings.ai.apiKey = apiKey; state.settings.ai.apiUrl = apiUrl; state.settings.ai.model = model; state.settings.ai.apiType = apiType; state.settings.ai.customHeaders = customHeaders; state.settings.ai.role = role; if (statusDiv) { statusDiv.textContent = "配置已保存。"; statusDiv.style.color = "#34a853"; } setTimeout(() => { dialog.style.display = "none"; }, 1000); }); } }, 100); // 测试AI连接的辅助函数 async function testAiConnection(apiKey, apiUrl, model, apiType = "openai", customHeaders = "") { return new Promise((resolve) => { let request; try { request = buildAiApiRequest({ apiType, apiUrl, apiKey, model, systemRole: "You are a helpful assistant.", message: "你好,请回复‘连接成功’即可。", maxTokens: 50, customHeaders }); } catch (error) { resolve({ success: false, message: error.message }); return; } GM_xmlhttpRequest({ method: "POST", url: request.url, headers: request.headers, data: JSON.stringify(request.data), timeout: 30000, onload: (response) => { try { const result = JSON.parse(response.responseText || "{}"); if (response.status < 200 || response.status >= 300) { throw new Error("HTTP错误 " + response.status + ": " + (result?.error?.message || result?.message || "未知错误")); } resolve({ success: true, message: extractAiResponse(result, apiType) }); } catch (error) { resolve({ success: false, message: error.message + ",原始响应: " + String(response.responseText || "").substring(0, 200) }); } }, onerror: () => resolve({ success: false, message: "网络请求失败,请检查网络连接和 API 地址" }), ontimeout: () => resolve({ success: false, message: "请求超时(30秒),请检查 API 地址是否正确" }) }); }); } return dialog; } function createSettingItem(title, description, controlGetter) { const settingItem = document.createElement("div"); settingItem.className = "setting-item"; settingItem.style.cssText = ` padding: 12px; border-radius: 10px; margin-bottom: 12px; background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border: 1px solid rgba(0, 123, 255, 0.1); display: flex; flex-direction: column; `; const titleElement = document.createElement("h4"); titleElement.textContent = title; titleElement.style.cssText = ` margin: 0 0 4px; color: #1f2937; font-size: 15px; font-weight: 500; `; const descElement = document.createElement("p"); descElement.textContent = description; descElement.style.cssText = ` margin: 0; color: #666; font-size: 13px; line-height: 1.4; `; const descriptionContainer = document.createElement("div"); descriptionContainer.style.cssText = ` display: flex; justify-content: space-between; align-items: center; width: 100%; `; const textContainer = document.createElement("div"); textContainer.append(titleElement, descElement); descriptionContainer.append(textContainer); settingItem.append(descriptionContainer); settingItem.addEventListener("click", () => { const control = controlGetter(); if (control && typeof control.focus === "function") { control.focus(); } }); return { settingItem, descriptionContainer, }; } function createToggleSwitch( id, isChecked, onChange ) { const container = document.createElement("div"); container.className = "toggle-container"; container.style.cssText = "display: flex; justify-content: space-between; align-items: center;"; const switchContainer = document.createElement("div"); switchContainer.className = "toggle-switch"; switchContainer.style.cssText = ` position: relative; width: 50px; height: 26px; border-radius: 13px; background-color: ${isChecked ? "rgba(0, 123, 255, 0.9)" : "#e5e7eb"}; cursor: pointer; `; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = `toggle-${id}`; checkbox.checked = isChecked; checkbox.style.display = "none"; const slider = document.createElement("span"); slider.className = "toggle-slider"; slider.style.cssText = ` position: absolute; top: 3px; left: ${isChecked ? "27px" : "3px"}; width: 20px; height: 20px; border-radius: 50%; background-color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.2); transition: none; `; const forceUpdateUI = (checked) => { checkbox.checked = checked; switchContainer.style.backgroundColor = checked ? "rgba(0, 123, 255, 0.9)" : "#e5e7eb"; slider.style.left = checked ? "27px" : "3px"; }; checkbox.addEventListener("change", () => { let allowChange = true; if (onChange) { allowChange = onChange(checkbox.checked) !== false; } if (!allowChange) { forceUpdateUI(!checkbox.checked); return; } forceUpdateUI(checkbox.checked); }); switchContainer.addEventListener("click", () => { const newState = !checkbox.checked; if (onChange) { if (onChange(newState) !== false) { forceUpdateUI(newState); } } else { forceUpdateUI(newState); } }); switchContainer.append(checkbox, slider); container.append(switchContainer); return container; } function createTextButton(text, backgroundColor, onClick) { const button = document.createElement("button"); button.textContent = text; button.style.cssText = ` padding: 9px 18px; border-radius: 8px; border: none; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; background: ${backgroundColor}; color: white; `; button.addEventListener("click", onClick); return button; } function addFocusBlurEffects(element) { element.addEventListener("focus", () => { element.style.borderColor = "rgba(0, 123, 255, 0.7)"; element.style.boxShadow = "0 0 0 3px rgba(0, 123, 255, 0.2)"; }); element.addEventListener("blur", () => { element.style.borderColor = "#d1d5db"; element.style.boxShadow = "none"; }); } function setActiveTab(tab, panel) { const tabs = document.querySelectorAll(".settings-tab"); const panels = [ document.getElementById("ai-settings-panel"), document.getElementById("advanced-settings-panel"), ]; tabs.forEach((t) => { t.classList.remove("active"); t.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; t.style.color = "#333"; }); panels.forEach((p) => { p.style.display = "none"; }); tab.classList.add("active"); tab.style.backgroundColor = "rgba(0, 123, 255, 0.9)"; tab.style.color = "white"; panel.style.display = "block"; } function showNotification(message, type = "success") { const notification = document.createElement("div"); const bgColor = type === "success" ? "rgba(40, 167, 69, 0.9)" : "rgba(220, 53, 69, 0.9)"; notification.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: ${bgColor}; color: white; padding: 10px 15px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 9999999; opacity: 0; transition: opacity 0.3s ease; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => (notification.style.opacity = "1"), 10); setTimeout(() => { notification.style.opacity = "0"; setTimeout(() => document.body.removeChild(notification), 300); }, 2000); } const Core = { CONFIG, messageObserver: null, lastProcessedMessage: null, processingMessage: false, currentMonitoredHR: null, domCache: {}, scrollStopRequested: false, stopAutoScroll: null, cancelActiveScroll() { this.scrollStopRequested = true; if (typeof this.stopAutoScroll === "function") { this.stopAutoScroll(); } const currentTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0; window.scrollTo({ top: currentTop, behavior: "auto" }); }, getAiMatchConfigIssue() { if (settings.deliveryMode !== "ai-match") return ""; const resumeText = (state.settings.resumeText || "").trim(); const apiUrl = (localStorage.getItem("aiApiUrl") || "").trim(); const model = (localStorage.getItem("aiModel") || "").trim(); if (resumeText.length < 50) return "未保存可用于匹配的简历文本"; if (!apiUrl || !model) return "未填写 AI API 地址或模型名称"; return ""; }, extractCurrentJobRequirement(card) { const textFrom = (root, selectors) => this.pickRecordText(root, selectors); const detailRoot = document.querySelector(".job-detail, .job-detail-box, [class*=job-detail]"); const jobTitle = textFrom(card, [".job-name", "[class*=job-name]"]) || textFrom(detailRoot, [".job-name", "h1", "[class*=job-name]"]); const company = textFrom(card, [".company-name", "[class*=company-name]"]) || textFrom(detailRoot, [".company-name", "[class*=company-name]"]); const salary = textFrom(card, [".salary", "[class*=salary]"]) || textFrom(detailRoot, [".salary", "[class*=salary]"]); const location = textFrom(card, [".job-address-desc", ".job-area", ".company-location", "[class*=job-area]"]) || textFrom(detailRoot, [".job-address-desc", ".job-area", ".company-location", "[class*=job-area]"]); const detailSelectors = [ ".job-detail .job-sec-text", ".job-sec-text", ".job-description", "[class*=job-description]", ".job-detail-box", "[class*=job-detail-content]", "[class*=job-detail] .job-detail-text" ]; const detailTexts = []; detailSelectors.forEach((selector) => { document.querySelectorAll(selector).forEach((element) => { const text = this.cleanRecordText(element.innerText || element.textContent || ""); if (text.length >= 40 && !detailTexts.includes(text)) detailTexts.push(text); }); }); const cardText = this.cleanRecordText(card?.innerText || card?.textContent || ""); const detailText = detailTexts.join("\n"); const summaryParts = [ jobTitle && "岗位名称:" + jobTitle, company && "公司:" + company, salary && "薪资:" + salary, location && "地点:" + location, cardText && "职位卡片信息:" + cardText, detailText && "岗位职责与要求:" + detailText ].filter(Boolean); const text = this.cleanRecordText(summaryParts.join("\n")); return { text: text.slice(0, 6000), hasDetail: detailText.length >= 50, jobTitle: jobTitle || "未识别岗位名称" }; }, parseAiMatchResult(responseText) { if (typeof responseText !== "string" || !responseText.trim()) return null; const normalized = responseText .trim() .replace(/^```(?:json)?\s*/i, "") .replace(/\s*```$/i, "") .trim(); const jsonMatch = normalized.match(/\{[\s\S]*?\}/); if (!jsonMatch) return null; try { const parsed = JSON.parse(jsonMatch[0]); const rawScore = Number(parsed.score); const decision = String(parsed.decision || "").trim(); if (!Number.isFinite(rawScore) || !decision) return null; return { score: Math.min(100, Math.max(0, Math.round(rawScore))), decision, reason: this.cleanRecordText(String(parsed.reason || "")) .slice(0, 140) }; } catch (_) { return null; } }, async evaluateJobMatch(card) { const configIssue = this.getAiMatchConfigIssue(); if (configIssue) return { allowed: false, score: null, reason: configIssue }; const job = this.extractCurrentJobRequirement(card); if (!job.text || !job.hasDetail) { return { allowed: false, score: null, reason: "未读取到完整岗位职责或任职要求,按保守策略跳过" }; } const resumeText = this.truncateResumeText((state.settings.resumeText || "").trim(), 6000); const threshold = Math.min(95, Math.max(50, parseInt(settings.aiMatchThreshold, 10) || 70)); const prompt = [ "请评估下面的简历与岗位需求是否匹配,用于决定是否发起求职沟通。", "仅依据所给材料,重点比较技能、项目/工作经历、年限、行业、地点和薪资等明确条件;信息不完整时要保守评分,不得臆测。", "不得依据或推断性别、年龄、籍贯、民族、婚育、健康状况等与岗位能力无关的敏感特征。", "只返回 JSON,不要 Markdown、解释或额外字段,格式严格为:{\"score\":0," + "\"decision\":\"投递\"," + "\"reason\":\"不超过40字\"}", "decision 只能是 投递 或 跳过;当关键要求不明确、资料不足或不匹配时使用 跳过。", "", "【岗位信息】", job.text, "", "【候选人简历】", resumeText ].join("\n"); try { this.log("正在进行 AI 岗位匹配:" + job.jobTitle); const response = await this.requestAi( prompt, "你是严谨的求职岗位匹配评估助手,只能基于提供材料评分,不得臆测。" ); const result = this.parseAiMatchResult(response); if (!result) { return { allowed: false, score: null, reason: "AI 未返回可解析的匹配结果,按保守策略跳过" }; } const recommendsDelivery = !/(跳过|不投递|不推荐)/.test(result.decision) && /投递|通过|推荐/.test(result.decision); const allowed = recommendsDelivery && result.score >= threshold; return { allowed, score: result.score, reason: result.reason || (allowed ? "匹配度达到阈值" : "未达到匹配阈值或 AI 建议跳过"), threshold }; } catch (error) { this.log("AI 岗位匹配失败:" + error.message); return { allowed: false, score: null, reason: "AI 匹配请求失败,按保守策略跳过" }; } }, // 检查薪资范围 checkSalaryRange(salaryText, range) { if (!salaryText || range.min === 0 && range.max === 100) { return true; // 未设置筛选条件,全部通过 } // 提取薪资数字(支持 10-20K、15K·14薪、20-30k 等格式) const salaryMatch = salaryText.match(/(\d+)(?:\s*-\s*(\d+))?/i); if (!salaryMatch) return true; // 无法解析,默认通过 const minSalary = parseInt(salaryMatch[1]); const maxSalary = salaryMatch[2] ? parseInt(salaryMatch[2]) : minSalary; // 检查是否在范围内 return minSalary >= range.min && maxSalary <= range.max; }, // 判断招聘者活跃度是否在用户选择的时间范围内。 // 数字越小表示越活跃,因此选择“3日内活跃”会同时接受今日、刚刚活跃和在线。 matchRecruiterActivity(activeTime, filters) { if (!Array.isArray(filters) || filters.length === 0 || filters.includes("不限")) { return true; } const text = String(activeTime || "").replace(/\s+/g, ""); const activityRank = { "在线": 0, "刚刚活跃": 0, "今日活跃": 1, "3日内活跃": 2, "本周活跃": 3, "本月活跃": 4, "半年前活跃": 5, }; let currentRank = null; if (/在线|刚刚活跃/.test(text)) currentRank = 0; else if (/今日|今天/.test(text)) currentRank = 1; else if (/3日内|3天内/.test(text)) currentRank = 2; else if (/本周/.test(text)) currentRank = 3; else if (/本月/.test(text)) currentRank = 4; else if (/半年/.test(text)) currentRank = 5; return filters.some((filter) => { const limit = activityRank[filter]; if (typeof limit === "number" && currentRank !== null) { return currentRank <= limit; } return text.includes(filter); }); }, // 检查经验要求是否符合筛选条件 checkExperienceMatch(experienceText, filter) { if (!filter || filter.includes("不限")) { return true; } if (!experienceText) { return filter.includes("无要求") || filter.includes("不限"); } const expLower = experienceText.toLowerCase(); // 经验筛选映射 const experienceMapping = { "应届生": ["应届", "在校", "实习", "无经验", "经验不限"], "1年以内": ["1年", "1年以下", "不满1年", "小于1年"], "1-3年": ["1-3年", "1~3年", "1~3年", "1至3年", "1-5年", "1~5年"], "3-5年": ["3-5年", "3~5年", "3至5年", "3-10年", "3~10年"], "5-10年": ["5-10年", "5~10年", "5至10年", "10年以下"], "10年以上": ["10年以上", "10年~", "10年及以上"] }; for (const filterItem of filter) { if (filterItem === "不限") continue; const matchedKeywords = experienceMapping[filterItem] || []; const hasMatch = matchedKeywords.some(keyword => expLower.includes(keyword)); if (hasMatch) { return true; } } return false; }, getCachedElement(selector, forceRefresh = false) { if (forceRefresh || !this.domCache[selector]) { this.domCache[selector] = document.querySelector(selector); } return this.domCache[selector]; }, getCachedElements(selector, forceRefresh = false) { if (forceRefresh || !this.domCache[selector + "[]"]) { this.domCache[selector + "[]"] = document.querySelectorAll(selector); } return this.domCache[selector + "[]"]; }, clearDomCache() { this.domCache = {}; }, async startProcessing() { // 在开始投递前,先滚动加载职位 // 注意:筛选条件(工作经验、学历、薪资)已经在用户选择时同步到BOSS页面 // 不需要在这里重复设置,否则会导致之前的选择被取消 if (location.pathname.includes("/jobs")) { await this.autoScrollJobList(); // 逐条处理职位,直到完成或被停止 while (state.isRunning && state.currentIndex < (state.jobList?.length || Infinity)) { await this.processJobList(); if (!state.isRunning) break; // 如果 jobList 被重置(如切换城市),重新加载 if (!state.jobList || state.jobList.length === 0) { await this.delay(CONFIG.BASIC_INTERVAL); } await this.delay(CONFIG.BASIC_INTERVAL / 2); } } else if (location.pathname.includes("/chat")) { // 启动聊天列表监听,检测新消息并自动切换 await this.processNewMessagesOnly(); this.setupChatListObserver(); } }, // 监听聊天列表变化,检测新消息并自动切换(替代轮询) setupChatListObserver() { if (this.chatListObserver) { this.chatListObserver.disconnect(); } const userList = document.querySelector(".user-list-content") || document.querySelector('.ul[role="group"]') || document.querySelector(".chat-list") || document.querySelector(".friend-list"); if (!userList) { this.log("未找到聊天列表容器,聊天监听未启动"); return; } this.chatListObserver = new MutationObserver(async () => { if (!state.isRunning) return; // 检测到列表变化(新消息、新会话等),尝试处理 await this.processNewMessagesOnly(); }); this.chatListObserver.observe(userList, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["class"], }); this.log("聊天列表监听已启动(事件驱动,无需轮询)"); }, async processNewMessagesOnly() { // 获取当前激活的聊天:通过右侧聊天面板的标题来识别 const activeHR = await this.getActiveChatFromHeader(); let targetLi = null; let hrKey = ""; if (activeHR) { // 方式1:通过右侧聊天头部的姓名匹配左侧列表 targetLi = await this.getLatestChatLi(activeHR.name); hrKey = `${activeHR.name}-${activeHR.company}`.toLowerCase(); } if (!activeHR || !targetLi) { // 方式2:兜底取最新消息 targetLi = await this.waitForElement(this.getLatestChatLi); if (!targetLi) { this.log("未找到聊天窗口"); return; } const nameEl = targetLi.querySelector(".name-text"); const companyEl = targetLi.querySelector(".name-box span:nth-child(2)"); const name = (nameEl?.textContent || "未知").trim(); const company = (companyEl?.textContent || "").trim(); hrKey = `${name}-${company}`.toLowerCase(); } // 如果当前监控的是同一个人,跳过 if (this.currentMonitoredHR === hrKey && this.messageObserver) { return; } // 切换到新的 HR:断开旧监听 if (this.currentMonitoredHR && this.currentMonitoredHR !== hrKey) { this.log(`切换到新聊天: ${hrKey}`); } this.currentMonitoredHR = hrKey; this.resetMessageState(); if (this.messageObserver) { this.messageObserver.disconnect(); this.messageObserver = null; } // 检查关键词过滤 if ( settings.communicationIncludeKeywords && settings.communicationIncludeKeywords.trim() ) { await this.simulateClick(targetLi.querySelector(".figure")); await this.delay(CONFIG.OPERATION_INTERVAL * 2); const positionName = this.getPositionName(); const includeKeywords = settings.communicationIncludeKeywords .toLowerCase() .split(/[,]/) .map((kw) => kw.trim()) .filter((kw) => kw.length > 0); const positionNameLower = positionName.toLowerCase(); const isMatch = includeKeywords.some((keyword) => positionNameLower.includes(keyword) ); if (!isMatch) { this.log(`跳过岗位,不含关键词[${includeKeywords.join(", ")}] - ${positionName}`); if (settings.communicationMode === 'suggested-reply') { await this._suggestDeclineForMismatch(positionName, includeKeywords); } return; } } // 点击并处理 if (!targetLi.classList.contains("last-clicked")) { await this.simulateClick(targetLi.querySelector(".figure")); targetLi.classList.add("last-clicked"); await this.delay(CONFIG.OPERATION_INTERVAL); if (settings.communicationMode === 'suggested-reply') { this.log(`建议回复模式: 已打开聊天窗口,等待新消息后生成回复建议`); } else { await HRInteractionManager.handleHRInteraction(hrKey); } } // 设置消息监听 await this.setupMessageObserver(hrKey); this.log(`正在监听新消息: ${hrKey}`); }, // 从右侧聊天面板头部获取当前激活的 HR 信息 async getActiveChatFromHeader() { try { const headerSelectors = [ '.chat-header .name-text', '.chat-header .name', '.chat-title .name', '.header-name', '.friend-name', ]; let nameEl = null; for (const sel of headerSelectors) { nameEl = document.querySelector(sel); if (nameEl && nameEl.textContent.trim()) break; } const companySelectors = [ '.chat-header .company-name', '.chat-header .company', '.chat-title .company', '.name-box span:nth-child(2)', ]; let companyEl = null; for (const sel of companySelectors) { companyEl = document.querySelector(sel); if (companyEl && companyEl.textContent.trim()) break; } const name = (nameEl?.textContent || "").trim(); const company = (companyEl?.textContent || "").trim(); if (name) { return { name, company }; } } catch (e) { // 忽略错误,兜底用旧方式 } return null; }, async getCurrentHRName() { // 尝试多种选择器获取HR名称 const selectors = [ '.chat-header .name-text', '.chat-header .name', '.chat-title .name', '.chat-basic-info .name', '.header-name', '.user-name', '.friend-name', '.chat-user-name', '.name-box .name-text', '[class*="name"]' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element) { const text = element.textContent.trim(); if (text && text.length > 0 && text !== "未知") { return text; } } } return "未知"; }, async getCurrentHRCompany() { // 尝试多种选择器获取公司名称 const selectors = [ '.chat-header .company-name', '.chat-header .company', '.chat-title .company', '.chat-basic-info .company', '.company-name', '.user-company', '.friend-company', '.name-box span:nth-child(2)', '[class*="company"]' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element) { const text = element.textContent.trim(); if (text && text.length > 0) { return text; } } } return ""; }, async scrollToLoadAllChats() { // 找到聊天列表容器 - 使用截图中的class const userListContent = document.querySelector(".user-list-content") || document.querySelector(".chat-list") || document.querySelector('ul[role="group"]') || document.querySelector(".friend-list") || document.querySelector(".message-list"); if (!userListContent) { this.log("未找到聊天列表容器"); return; } this.log("正在加载更多聊天窗口..."); let previousCount = 0; let sameCountTimes = 0; const maxAttempts = 10; for (let i = 0; i < maxAttempts; i++) { if (!state.isRunning) break; // 使用截图中的class: friend-content const currentItems = userListContent.querySelectorAll('.friend-content, .friend-content.selected'); const currentCount = currentItems.length; this.log(`已加载 ${currentCount} 个聊天窗口...`); if (currentCount === previousCount) { sameCountTimes++; if (sameCountTimes >= 3) { this.log("聊天窗口加载完成"); break; } } else { sameCountTimes = 0; } previousCount = currentCount; // 滚动到底部加载更多 userListContent.scrollTo({ top: userListContent.scrollHeight, behavior: 'smooth' }); await this.delay(1000); } // 滚动回顶部 userListContent.scrollTo({ top: 0, behavior: 'smooth' }); await this.delay(500); }, // 获取聊天面板消息容器(多选择器兜底) getChatMessageContainer() { // 首先尝试找到聊天详情区域的容器(不是左侧列表) // BOSS直聘结构:左侧是聊天列表,右侧是聊天详情 const chatDetailSelectors = [ '.chat-message .im-list', '.chat-content .im-list', '.chat-detail .im-list', '.chat-window .im-list', '.message-container .im-list', '.chat-message [class*="im-list"]', '.chat-content [class*="im-list"]', '[class*="chat-detail"] [class*="im-list"]', '[class*="chat-message"] [class*="im-list"]', '[class*="chat-content"] [class*="im-list"]', ]; for (const sel of chatDetailSelectors) { const el = document.querySelector(sel); if (el && el.querySelectorAll('li').length > 0) { // 验证这是消息列表而不是聊天列表 // 消息列表的li应该有 message-item 类 const firstLi = el.querySelector('li'); if (firstLi && (firstLi.className.includes('message-item') || firstLi.className.includes('item-'))) { return el; } } } // 备选:查找包含消息项的容器 const allImLists = document.querySelectorAll('.im-list, [class*="im-list"]'); for (const el of allImLists) { if (el.querySelectorAll('li').length > 0) { const firstLi = el.querySelector('li'); const liClass = firstLi?.className || ''; // 确保是消息列表(包含 item-myself, item-friend, item-system 等) if (liClass.includes('item-myself') || liClass.includes('item-friend') || liClass.includes('item-system') || liClass.includes('message-item')) { return el; } } } return null; }, // 渐进式向上滚动聊天面板,触发虚拟列表加载历史消息 async scrollChatToLoadHistory() { const imList = this.getChatMessageContainer(); if (!imList) return; const initialCount = imList.querySelectorAll('li').length; const maxScrolls = 20; let lastMsgCount = initialCount; for (let i = 0; i < maxScrolls; i++) { const currentTop = imList.scrollTop; if (currentTop <= 0) break; // 已到顶部 // 向上滚动一大步 imList.scrollBy({ top: -600, behavior: 'instant' }); await this.delay(500); const newMsgCount = imList.querySelectorAll('li').length; if (newMsgCount > lastMsgCount) { lastMsgCount = newMsgCount; } else if (i >= 3 && newMsgCount === lastMsgCount) { // 连续几次没新消息,可能已经加载完毕 if (imList.scrollTop <= 0) break; } } // 最后确保滚到顶部 imList.scrollTo({ top: 0, behavior: 'instant' }); await this.delay(300); const finalCount = imList.querySelectorAll('li').length; if (finalCount > initialCount) { this.log(`[历史加载] 消息从${initialCount}条增加到${finalCount}条`); } }, async handleSingleChat(hrKey, positionName) { try { // 获取最后一条HR消息 const lastMessage = await this.getLastFriendMessageText(); if (!lastMessage) { this.log(`${hrKey}: 未找到HR消息`); return; } this.log(`${hrKey} 的最后消息: ${lastMessage.slice(0, 30)}...`); // 检查是否已经回复过这条消息 const messageKey = `${hrKey}-${this.cleanMessage(lastMessage)}`; if (this.lastProcessedMessage === messageKey) { this.log(`${hrKey}: 已回复过此消息`); return; } // 检测HR是否明确拒绝 const rejectionPatterns = [ "不合适", "不符合", "不需要", "暂时不招", "已招满", "谢谢", "再见", "不合适", "不符合条件", "不符合要求", "我们要求", "您的条件", "经验不足", "学历不够", "暂不匹配", "不匹配", "不太合适", "不是很合适", "暂时没有", "暂无需求", "没有需求", "不需要人", "已经招到", "已经招满", "编制已满", "坑位已满", "已结束", "已截止", "职位已下", "岗位已撤" ]; const isRejection = rejectionPatterns.some(pattern => lastMessage.includes(pattern) ); if (isRejection) { this.log(`检测到HR拒绝,停止对话: "${lastMessage.slice(0, 30)}..."`); // 关闭聊天窗口 const chatItem = document.querySelector(`[data-hr-key="${hrKey}"]`); if (chatItem) { const closeBtn = chatItem.querySelector(".close-btn, [class*='close']"); if (closeBtn) closeBtn.click(); } return; } // 生成个性化回复 const replyText = await this.generatePersonalizedReply(lastMessage); if (!replyText) { this.log(`${hrKey}: 无法生成回复`); return; } // 发送回复 const inputBox = await this.waitForElement("#chat-input"); if (!inputBox) { this.log(`${hrKey}: 未找到输入框`); return; } inputBox.textContent = ""; inputBox.focus(); document.execCommand("insertText", false, replyText); await this.delay(500); const sendButton = document.querySelector(".btn-send"); if (sendButton) { await this.simulateClick(sendButton); this.log(`已回复 ${hrKey}: ${replyText.slice(0, 30)}...`); // 记录已处理 this.lastProcessedMessage = messageKey; if (!this.repliedMessages) { this.repliedMessages = new Set(); } this.repliedMessages.add(messageKey); state.hrInteractions.processedHRs.add(hrKey); if (!state.hrInteractions.lastMessageTime) { state.hrInteractions.lastMessageTime = {}; } state.hrInteractions.lastMessageTime[hrKey] = Date.now(); StatePersistence.saveState(); } // 如果HR提到简历,发送简历 if (lastMessage.includes("简历") || lastMessage.includes("发送")) { await this.delay(1000); const sent = await HRInteractionManager.sendResume(); if (sent) { this.log(`已向 ${hrKey} 发送简历`); } } } catch (error) { this.log(`处理 ${hrKey} 的聊天出错: ${error.message}`); } }, async autoScrollJobList() { return new Promise((resolve) => { const cardSelector = "li.job-card-box"; const maxHistory = 3; const waitTime = CONFIG.BASIC_INTERVAL; const cardCountHistory = []; let settled = false; const finish = (result) => { if (settled) return; settled = true; this.stopAutoScroll = null; resolve(result); }; const scrollStep = async () => { if (this.scrollStopRequested || !state.isRunning) { finish(null); return; } window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "smooth", }); await this.delay(waitTime); if (this.scrollStopRequested || !state.isRunning) { finish(null); return; } const cards = document.querySelectorAll(cardSelector); cardCountHistory.push(cards.length); if (cardCountHistory.length > maxHistory) cardCountHistory.shift(); if ( cardCountHistory.length === maxHistory && new Set(cardCountHistory).size === 1 ) { this.log("当前页面岗位加载完成,开始沟通"); finish(cards); return; } void scrollStep(); }; this.stopAutoScroll = () => finish(null); void scrollStep(); }); }, async processJobList() { if (!state.isRunning) return; // 基础模式完全不依赖 AI 或简历;AI 模式在开始前先校验,避免配置缺失时盲投。 if (settings.deliveryMode === "ai-match") { const configIssue = this.getAiMatchConfigIssue(); if (configIssue) { const message = "AI 匹配模式未启动:" + configIssue + "。为避免盲投,未向任何岗位发起沟通。"; this.log(message); showNotification(message, "warning"); toggleProcess(); return; } } const activeStatusFilter = settings.recruiterActivityStatus; const experienceFilter = settings.experienceFilter || ["不限"]; if (!state.jobList || state.jobList.length === 0) { const excludeHeadhunters = settings.excludeHeadhunters; const salaryRange = settings.salaryRange || { min: 0, max: 100 }; // 先滚动页面加载更多职位 await this.scrollToLoadMoreJobs(); state.jobList = Array.from( document.querySelectorAll("li.job-card-box") ).filter((card) => { const title = card.querySelector(".job-name")?.textContent?.toLowerCase() || ""; const addressText = ( card.querySelector(".job-address-desc")?.textContent || card.querySelector(".company-location")?.textContent || card.querySelector(".job-area")?.textContent || "" ) .toLowerCase() .trim(); const headhuntingElement = card.querySelector(".job-tag-icon"); const altText = headhuntingElement ? headhuntingElement.alt : ""; // 猎头检测 - 检查多种可能表示猎头的元素 const headhuntingKeywords = ["猎头", "猎头职位", "猎", "headhunter", "人力资源", "外包", "外包职位"]; const companyText = card.querySelector(".company-name")?.textContent?.toLowerCase() || ""; const tagTexts = Array.from(card.querySelectorAll(".job-tag, .tag, [class*='tag']")) .map(el => el.textContent?.toLowerCase() || "") .join(" "); const allText = (altText + " " + companyText + " " + tagTexts).toLowerCase(); const isHeadhunting = headhuntingKeywords.some(keyword => allText.includes(keyword)); const includeMatch = state.includeKeywords.length === 0 || state.includeKeywords.some((kw) => kw && title.includes(kw.trim())); const locationMatch = state.locationKeywords.length === 0 || state.locationKeywords.some( (kw) => kw && addressText.includes(kw.trim()) ); const excludeHeadhunterMatch = !excludeHeadhunters || !isHeadhunting; // 薪资筛选 const salaryText = card.querySelector(".salary")?.textContent || ""; const salaryMatch = this.checkSalaryRange(salaryText, salaryRange); // 经验筛选 const experienceText = ( card.querySelector(".job-exp")?.textContent || card.querySelector(".experience")?.textContent || card.querySelector("[class*='exp']")?.textContent || "" ).trim().toLowerCase(); const experienceMatch = this.checkExperienceMatch(experienceText, experienceFilter); return includeMatch && locationMatch && excludeHeadhunterMatch && salaryMatch && experienceMatch; }); if (!state.jobList.length) { this.log("没有符合条件的职位"); toggleProcess(); return; } this.log(`已加载 ${state.jobList.length} 个符合条件的职位`); } if (state.currentIndex >= state.jobList.length) { this.resetCycle(); state.jobList = []; return; } if (!state.isRunning) return; const currentCard = state.jobList[state.currentIndex]; currentCard.scrollIntoView({ behavior: "smooth", block: "center" }); currentCard.click(); await this.delay(CONFIG.OPERATION_INTERVAL * 2); if (!state.isRunning) return; let activeTime = "未知"; const onlineTag = document.querySelector(".boss-online-tag"); if (onlineTag && onlineTag.textContent.trim() === "在线") { activeTime = "在线"; } else { const activeTimeElement = document.querySelector(".boss-active-time"); activeTime = activeTimeElement?.textContent?.trim() || "未知"; } const isActiveStatusMatch = this.matchRecruiterActivity( activeTime, activeStatusFilter ); if (!isActiveStatusMatch) { this.log(`跳过: 招聘者状态 "${activeTime}"`); state.currentIndex++; return; } // 只有用户主动开启 AI 匹配模式时,才会发送简历和岗位信息给已配置的 AI 服务。 if (settings.deliveryMode === "ai-match") { const matchResult = await this.evaluateJobMatch(currentCard); if (!state.isRunning) return; if (!matchResult.allowed) { const scoreText = Number.isFinite(matchResult.score) ? matchResult.score + " 分" : "未评分"; this.log("AI 匹配跳过:" + scoreText + "," + (matchResult.reason || "未达到匹配阈值")); state.currentIndex++; return; } this.log( "AI 匹配通过:" + matchResult.score + " 分(阈值 " + (matchResult.threshold || settings.aiMatchThreshold) + " 分)," + (matchResult.reason || "建议投递") ); } const includeLog = state.includeKeywords.length ? `职位名包含[${state.includeKeywords.join("、")}]` : "职位名不限"; const locationLog = state.locationKeywords.length ? `工作地包含[${state.locationKeywords.join("、")}]` : "工作地不限"; const salaryRange = settings.salaryRange || { min: 0, max: 100 }; const salaryLog = (salaryRange.min > 0 || salaryRange.max < 100) ? `薪资${salaryRange.min}K-${salaryRange.max}K` : "薪资不限"; state.currentIndex++; this.log( `正在沟通:${state.currentIndex}/${state.jobList.length },${includeLog},${locationLog},${salaryLog},招聘者"${activeTime}"` ); const chatBtn = document.querySelector("a.op-btn-chat"); if (chatBtn) { const btnText = chatBtn.textContent.trim(); if (btnText === "立即沟通") { if (!state.isRunning) return; chatBtn.click(); await this.delay(CONFIG.OPERATION_INTERVAL); if (!state.isRunning) return; await this.handleGreetingModal(); await this.delay(1000); if (!state.isRunning) return; await this.recordApplication(currentCard, activeTime); } } }, cleanRecordText(text) { return (text || "") .replace(/\s+/g, " ") .replace(/[​-‍]/g, "") .trim(); }, pickRecordText(root, selectors) { for (const selector of selectors) { const el = root?.querySelector?.(selector); const text = this.cleanRecordText(el?.textContent || ""); if (text) return text; } return ""; }, getRecordTexts(root, selectors) { const texts = []; selectors.forEach((selector) => { root?.querySelectorAll?.(selector).forEach((el) => { const text = this.cleanRecordText(el.textContent); if (text && !texts.includes(text)) texts.push(text); }); }); return texts; }, getCardLines(card) { const text = card?.innerText || card?.textContent || ""; return text .split(/\n+/) .map((line) => line.trim()) .filter(Boolean); }, isExperienceText(text) { return /^(经验不限|在校\/应届|应届|不限|[0-9]+\s*-\s*[0-9]+\s*年|[0-9]+年以内|[0-9]+年以上)$/.test(this.cleanRecordText(text)); }, isEducationText(text) { return /^(学历不限|本科|大专|硕士|博士|中专\/中技|中专|高中|初中及以下|初中|MBA|EMBA)$/.test(this.cleanRecordText(text)); }, isLocationText(text) { const line = this.cleanRecordText(text); if (!line || line.length > 18) return false; if (/公司|科技|股份|有限|智能|电子|软件|网络|信息|集团|招聘|经验|本科|薪|K/.test(line)) return false; if (line.includes("·")) return /北京|上海|广州|深圳|杭州|苏州|南京|成都|武汉|西安|重庆|天津|长沙|郑州|合肥|宁波|厦门|青岛|无锡|佛山|东莞|常州|珠海|中山|惠州|嘉兴|南通|昆山/.test(line); return /^(北京|上海|广州|深圳|杭州|苏州|南京|成都|武汉|西安|重庆|天津|长沙|郑州|合肥|宁波|厦门|青岛|无锡|佛山|东莞|常州|珠海|中山|惠州|嘉兴|南通|昆山)$/.test(line) || /(相城|虎丘|高新|滨江|余杭|萧山|上城|西湖|工业园|园区|新区|区|县|镇|街道)$/.test(line); }, findLocationFromLines(lines) { const exclude = /K|薪|经验|本科|大专|硕士|博士|学历|融资|上市|人|HR|招聘|沟通/; return lines.find((line) => !exclude.test(line) && this.isLocationText(line) ) || ""; }, findCompanyFromLines(lines, jobName, location) { const badLine = /立即沟通|继续沟通|刚刚活跃|今日活跃|在线|经验|不限|应届|在校|本科|大专|硕士|博士|学历|K|薪|区|市|县|收藏|地图|搜索|职位描述|职位要求|工作地址|猎头/; return lines.find((line) => line !== jobName && line !== location && line.length >= 2 && line.length <= 30 && !badLine.test(line) && !this.isExperienceText(line) && !this.isEducationText(line) && !this.isLocationText(line) ) || ""; }, sanitizeCompanyName(value, lines, jobName, location) { const companyName = this.cleanRecordText(value); const invalid = !companyName || companyName === jobName || companyName === location || this.isExperienceText(companyName) || this.isEducationText(companyName) || this.isLocationText(companyName) || /K|薪|经验|学历|刚刚活跃|今日活跃|在线|立即沟通|继续沟通/.test(companyName); return invalid ? this.findCompanyFromLines(lines, jobName, location) : companyName; }, async recordApplication(card, activeTime) { try { const detailRoot = document; const cardLines = this.getCardLines(card); const footer = card.querySelector(".job-card-footer, .job-card-bottom, .card-footer, .job-card-right, .company-info"); const footerLines = this.getCardLines(footer); const rawJobName = this.pickRecordText(card, [ ".job-name", ".job-title .job-name", ".job-title", ".position-name", "[class*='job-name']" ]) || cardLines[0] || ""; const jobName = rawJobName; // 地区 const rawLocation = this.pickRecordText(card, [ ".job-area", ".job-address-desc", ".job-area-wrapper", ".job-location", ".job-info .job-area", ".job-card-left .job-area", ".company-location" ]); const location = this.isLocationText(rawLocation) ? rawLocation : this.findLocationFromLines(footerLines) || this.findLocationFromLines(cardLines); // 公司名称 - 优先从 footer 选择器提取 const rawCompanyName = this.pickRecordText(footer || card, [ ".company-name", ".company-info .company-name", ".job-card-right .company-name", ".job-card-right .company-info h3", ".company-info h3", "h3.company-name", "a.company-name" ]); const companyName = this.sanitizeCompanyName( rawCompanyName, footerLines.length ? footerLines : cardLines, jobName, location ) || this.findCompanyFromLines(cardLines, jobName, location); // 经验和学历 - 从 .tag-list li/span 提取(BOSS直聘的关键做法) const jobTags = this.getRecordTexts(card, [ ".tag-list li", ".tag-list span", ".job-card-left .tag-list li", ".job-card-left .tag-list span", ".job-info .tag-list li", ".job-info .tag-list span", ".job-primary .tag-list li", ".job-primary .tag-list span" ]); const experience = jobTags.find((tag) => /经验|年|应届|在校|不限/.test(tag) && !/本科|大专|硕士|博士|学历|中专|高中|MBA|EMBA|初中/.test(tag) ) || ""; const education = jobTags.find((tag) => /本科|大专|硕士|博士|学历|中专|高中|MBA|EMBA|初中/.test(tag) ) || ""; // 公司标签(规模、融资阶段) const companyTags = this.getRecordTexts(card, [ ".company-tag-list li", ".company-tag-list span", ".company-info li", ".company-info span", ".company-desc li", ".company-desc span", ".job-card-right li", ".job-card-right span" ]); const companySize = companyTags.find((tag) => /\d+人|人以上|人以下/.test(tag)) || ""; const financingStage = companyTags.find((tag) => /未融资|融资|天使轮|A轮|B轮|C轮|D轮|上市|不需要融资/.test(tag)) || ""; // HR 职位信息 const hrPosition = this.pickRecordText(card, [ ".boss-title", ".boss-info .boss-title", ".job-boss-info .boss-title", ".job-card-footer .boss-title", ]) || this.pickRecordText(detailRoot, [ ".boss-title", ".boss-info .boss-title", ".boss-info-attr", ".boss-info .gray", ".job-boss-info .boss-title" ]) || ""; // 保存职位原始页链接,供数据面板回看。 const jobAnchor = card.querySelector("a[href*='job_detail'], a[href*='/job/']"); const sourceUrl = jobAnchor?.href || window.location.href; // 生成稳定 ID,优先使用职位原始链接。 const idBase = [companyName, jobName, location, sourceUrl].filter(Boolean).join("|"); const uid = typeof CryptoJS !== "undefined" && CryptoJS.MD5 ? `boss-${CryptoJS.MD5(idBase).toString()}` : `boss-${idBase.replace(/\s+/g, "").slice(0, 80)}`; const record = { id: uid, uid, companyName: companyName || "未知公司", jobName: jobName || "未知岗位", location: location || "", companySize, financingStage, experience, education, hrPosition: hrPosition || "", sourceUrl, status: "applied", applyTime: Date.now(), notes: "" }; // 更新内存 let records = state.applicationRecords || []; const existingIndex = records.findIndex((item) => item.id === record.id || item.uid === record.uid); if (existingIndex >= 0) { records[existingIndex] = { ...records[existingIndex], ...record }; } else { records.unshift(record); } if (records.length > 1000) { records = records.slice(0, 1000); } state.applicationRecords = records; // 保存到 localStorage StorageManager.setItem("applicationRecords", records); // 同步到 GM 存储(供看板页面读取),同时清除 lastClearedAt try { GM_setValue('applicationRecords', JSON.stringify(records)); GM_deleteValue('lastClearedAt'); } catch (e) { // GM 存储写入失败不阻塞主流程 } this.log(`已记录投递: ${companyName || "未知公司"} - ${jobName || "未知岗位"}`); } catch (e) { this.log(`记录投递信息失败: ${e.message}`); } }, async handleGreetingModal() { await this.delay(CONFIG.OPERATION_INTERVAL * 4); const btn = [ ...document.querySelectorAll(".default-btn.cancel-btn"), ].find((b) => b.textContent.trim() === "留在此页"); if (btn) { btn.click(); await this.delay(CONFIG.OPERATION_INTERVAL * 2); } }, async handleChatPage() { const latestChatLi = await this.waitForElement(this.getLatestChatLi); if (!latestChatLi) return; const nameEl = latestChatLi.querySelector(".name-text"); const companyEl = latestChatLi.querySelector( ".name-box span:nth-child(2)" ); const name = (nameEl?.textContent || "未知").trim(); const company = (companyEl?.textContent || "").trim(); const hrKey = `${name}-${company}`.toLowerCase(); // 如果当前正在监控同一个 HR,且 observer 正常,则跳过繁重逻辑 if (this.currentMonitoredHR === hrKey && this.messageObserver) { return; } this.currentMonitoredHR = hrKey; this.resetMessageState(); if (this.messageObserver) { this.messageObserver.disconnect(); this.messageObserver = null; } if ( settings.communicationIncludeKeywords && settings.communicationIncludeKeywords.trim() ) { await this.simulateClick(latestChatLi.querySelector(".figure")); await this.delay(CONFIG.OPERATION_INTERVAL * 2); const positionName = this.getPositionName(); const includeKeywords = settings.communicationIncludeKeywords .toLowerCase() .split(/[,,]/) .map((kw) => kw.trim()) .filter((kw) => kw.length > 0); const positionNameLower = positionName.toLowerCase(); const isMatch = includeKeywords.some((keyword) => positionNameLower.includes(keyword) ); if (!isMatch) { this.log(`跳过岗位,不含关键词[${includeKeywords.join(", ")}]`); if (settings.communicationMode === "auto") { await this.scrollUserList(); } return; } } if (!latestChatLi.classList.contains("last-clicked")) { await this.simulateClick(latestChatLi.querySelector(".figure")); latestChatLi.classList.add("last-clicked"); await this.delay(CONFIG.OPERATION_INTERVAL); await HRInteractionManager.handleHRInteraction(hrKey); if (settings.communicationMode === "auto") { await this.scrollUserList(); } } await this.setupMessageObserver(hrKey); }, async scrollUserList() { const userListContent = document.querySelector(".user-list-content"); if (userListContent) { const totalHeight = userListContent.scrollHeight; const clientHeight = userListContent.clientHeight; const maxScrollTop = totalHeight - clientHeight; if (maxScrollTop <= 0) { return; } const scrollSteps = Math.floor(Math.random() * 3) + 3; for (let i = 0; i < scrollSteps; i++) { const randomTop = Math.floor(Math.random() * maxScrollTop); userListContent.scrollTo({ top: randomTop, behavior: "smooth", }); const randomDelay = Math.floor(Math.random() * 2000) + 1000; await this.delay(randomDelay); } const finalPosition = Math.random() > 0.5 ? maxScrollTop : 0; userListContent.scrollTo({ top: finalPosition, behavior: "smooth", }); } }, resetMessageState() { this.lastProcessedMessage = null; this.processingMessage = false; if (!this.repliedMessages) { this.repliedMessages = new Set(); } }, async setBossFilters() { try { // 获取UI上选择的经验和学历 const expSelect = document.getElementById("experience-select"); const eduSelect = document.getElementById("education-select"); const salarySelect = document.getElementById("salary-filter"); const expValue = expSelect?.value || "不限"; const eduValue = eduSelect?.value || "不限"; const salaryValue = salarySelect?.value || "all"; // 检查页面上是否已经设置了相同的筛选条件 const currentFilters = this.getCurrentBossFilters(); // 经验筛选 if (expValue !== "不限" && !currentFilters.experience?.includes(expValue)) { const expMap = { "应届生": "应届生", "1年以内": "1年以内", "1-3年": "1-3年", "3-5年": "3-5年", "5-10年": "5-10年", "10年以上": "10年以上" }; const bossText = expMap[expValue] || expValue; // 点击经验按钮 const expFilterBtn = Array.from(document.querySelectorAll('.filter-select-box, .filter-item, [class*="filter"], button, div')) .find(el => el.textContent?.includes('经验') || el.textContent?.includes('工作年限')); if (expFilterBtn) { expFilterBtn.click(); await this.delay(500); const options = document.querySelectorAll('.filter-select-dropdown li, .dropdown-item, [class*="option"], li'); for (const option of options) { if (option.textContent?.includes(bossText)) { option.click(); this.log(`已设置BOSS经验筛选: ${bossText}`); break; } } await this.delay(300); } } // 学历筛选 if (eduValue !== "不限" && !currentFilters.education?.includes(eduValue)) { // 点击学历按钮 const eduFilterBtn = Array.from(document.querySelectorAll('.filter-select-box, .filter-item, [class*="filter"], button, div')) .find(el => el.textContent?.includes('学历') || el.textContent?.includes('教育')); if (eduFilterBtn) { eduFilterBtn.click(); await this.delay(500); const options = document.querySelectorAll('.filter-select-dropdown li, .dropdown-item, [class*="option"], li'); for (const option of options) { if (option.textContent?.includes(eduValue)) { option.click(); this.log(`已设置BOSS学历筛选: ${eduValue}`); break; } } await this.delay(300); } } // 薪资筛选 if (salaryValue !== "all" && !currentFilters.salary?.includes(salaryValue)) { const salaryMap = { "below-3k": "3K以下", "3-5k": "3-5K", "5-10k": "5-10K", "10-20k": "10-20K", "20-50k": "20-50K", "50k+": "50K以上" }; const bossText = salaryMap[salaryValue] || salaryValue; const salaryFilterBtn = Array.from(document.querySelectorAll('.filter-select-box, .filter-item, [class*="filter"], button, div')) .find(el => el.textContent?.includes('薪资') || el.textContent?.includes('薪资待遇')); if (salaryFilterBtn) { salaryFilterBtn.click(); await this.delay(500); const options = document.querySelectorAll('.filter-select-dropdown li, .dropdown-item, [class*="option"], li'); for (const option of options) { if (option.textContent?.includes(bossText)) { option.click(); this.log(`已设置BOSS薪资筛选: ${bossText}`); break; } } await this.delay(300); } } // 等待筛选结果加载 await this.delay(1000); } catch (error) { this.log(`设置BOSS筛选失败: ${error.message}`); } }, // 获取当前BOSS页面上已设置的筛选条件 getCurrentBossFilters() { const filters = { experience: [], education: [], salary: [] }; try { // 查找已选中的筛选标签 const selectedTags = document.querySelectorAll('.filter-select-box .selected, .filter-item .selected, .filter-tag.selected, [class*="selected"]'); selectedTags.forEach(tag => { const text = tag.textContent?.trim() || ''; if (text.includes('年') || text.includes('应届') || text.includes('经验')) { filters.experience.push(text); } else if (text.includes('学历') || ['初中', '中专', '高中', '大专', '本科', '硕士', '博士'].some(e => text.includes(e))) { filters.education.push(text); } else if (text.includes('K') || text.includes('薪') || text.includes('薪资')) { filters.salary.push(text); } }); // 也检查筛选按钮上的文字 const filterBtns = document.querySelectorAll('.filter-select-box, .filter-item, [class*="filter"]'); filterBtns.forEach(btn => { const text = btn.textContent?.trim() || ''; // 如果按钮文字不是默认的"经验"、"学历"、"薪资",说明已经设置了筛选 if (text.includes('经验') && !text.includes('经验不限') && text !== '经验') { const expMatch = text.match(/经验[::]?(.+)/); if (expMatch) filters.experience.push(expMatch[1]); } if (text.includes('学历') && !text.includes('学历不限') && text !== '学历') { const eduMatch = text.match(/学历[::]?(.+)/); if (eduMatch) filters.education.push(eduMatch[1]); } if (text.includes('薪资') && !text.includes('薪资不限') && text !== '薪资') { const salaryMatch = text.match(/薪资[::]?(.+)/); if (salaryMatch) filters.salary.push(salaryMatch[1]); } }); } catch (error) { console.log('获取当前筛选条件失败:', error); } return filters; }, async scrollToLoadMoreJobs() { const viewportHeight = window.innerHeight; // 停止后立即退出,不再执行后续平滑滚动或回到顶部。 for (let i = 0; i < 3; i++) { if (this.scrollStopRequested || !state.isRunning) return false; const currentScroll = window.scrollY || document.documentElement.scrollTop; const targetScroll = currentScroll + viewportHeight * 0.8; window.scrollTo({ top: targetScroll, behavior: "smooth" }); this.log(`正在加载更多职位... (${i + 1}/3)`); await this.delay(1500); if (this.scrollStopRequested || !state.isRunning) return false; } if (this.scrollStopRequested || !state.isRunning) return false; window.scrollTo({ top: 0, behavior: "smooth" }); await this.delay(500); return !this.scrollStopRequested && state.isRunning; }, async setupMessageObserver(hrKey) { const chatContainer = await this.waitForElement(".chat-message .im-list"); if (!chatContainer) return; this.messageObserver = new MutationObserver(async (mutations) => { let hasNewFriendMessage = false; for (const mutation of mutations) { if (mutation.type === "childList" && mutation.addedNodes.length > 0) { hasNewFriendMessage = Array.from(mutation.addedNodes).some((node) => node.classList?.contains("item-friend") ); if (hasNewFriendMessage) break; } } if (hasNewFriendMessage) { await this.handleNewMessage(hrKey); } }); this.messageObserver.observe(chatContainer, { childList: true, subtree: true, }); }, async handleNewMessage(hrKey) { if (!state.isRunning) return; if (this.processingMessage) return; this.processingMessage = true; try { await this.delay(CONFIG.OPERATION_INTERVAL); const lastMessage = await this.getLastFriendMessageText(); if (!lastMessage) return; const cleanedMessage = this.cleanMessage(lastMessage); const shouldSendResumeOnly = cleanedMessage.includes("简历"); if (cleanedMessage === this.lastProcessedMessage) return; this.lastProcessedMessage = cleanedMessage; this.log(`已同意交换,对方: ${lastMessage}`); await this.delay(CONFIG.DELAYS.MEDIUM_SHORT); const updatedMessage = await this.getLastFriendMessageText(); if ( updatedMessage && this.cleanMessage(updatedMessage) !== cleanedMessage ) { await this.handleNewMessage(hrKey); return; } const autoSendResume = settings.useAutoSendResume; const autoReplyEnabled = settings.autoReply; if (shouldSendResumeOnly && autoSendResume) { this.log('对方提到"简历",正在发送简历'); const sent = await HRInteractionManager.sendResume(); if (sent) { state.hrInteractions.sentResumeHRs.add(hrKey); StatePersistence.saveState(); this.log(`已向 ${hrKey} 发送简历`); } } else if (autoReplyEnabled) { await HRInteractionManager.handleHRInteraction(hrKey); } else if (settings.communicationMode === 'suggested-reply') { await this.suggestReply(); } await this.delay(CONFIG.DELAYS.MEDIUM_SHORT); } catch (error) { this.log(`处理消息出错: ${error.message}`); } finally { this.processingMessage = false; } }, cleanMessage(message) { if (!message) return ""; let clean = message.replace(/<[^>]*>/g, ""); clean = clean .trim() .replace(/\s+/g, " ") .replace(/[\u200B-\u200D\uFEFF]/g, ""); return clean; }, getLatestChatLi(targetName = "") { // 优先找当前激活的聊天(用户点击选中的) const activeSelectors = [ 'ul[role="group"] li[role="listitem"].active:has(.friend-content-warp)', 'ul[role="group"] li[role="listitem"].current:has(.friend-content-warp)', 'ul[role="group"] li[role="listitem"][aria-current="true"]:has(.friend-content-warp)', 'ul[role="group"] li[role="listitem"][aria-selected="true"]:has(.friend-content-warp)', 'ul[role="group"] li[role="listitem"].is-active:has(.friend-content-warp)', ]; for (const sel of activeSelectors) { const el = document.querySelector(sel); if (el) return el; } // 如果指定了姓名,尝试从列表中匹配 if (targetName) { const allItems = document.querySelectorAll( 'ul[role="group"] li[role="listitem"][class]:has(.friend-content-warp)' ); for (const item of allItems) { const nameEl = item.querySelector(".name-text"); if (nameEl && nameEl.textContent.trim() === targetName) { return item; } } } // 兜底:取第一个 return document.querySelector( 'ul[role="group"] li[role="listitem"][class]:has(.friend-content-warp)' ); }, getPositionName() { try { const positionNameElement = Core.getCachedElement(".position-name", true) || Core.getCachedElement(".job-name", true) || Core.getCachedElement( '[class*="position-content"] .left-content .position-name', true ) || document.querySelector(".position-name") || document.querySelector(".job-name"); if (positionNameElement) { return positionNameElement.textContent.trim(); } else { // Silent failure is better here as we might check multiple times return ""; } } catch (e) { Core.log(`获取岗位名称出错: ${e.message}`); return ""; } }, async suggestReply() { try { const lastMessage = await this.getLastFriendMessageText(); if (!lastMessage) return false; // 检测HR是否明确拒绝 const rejectionPatterns = [ "不合适", "不符合", "不需要", "暂时不招", "已招满", "谢谢", "再见", "不符合条件", "不符合要求", "我们要求", "您的条件", "经验不足", "学历不够", "暂不匹配", "不匹配", "不太合适", "不是很合适", "暂时没有", "暂无需求", "没有需求", "不需要人", "已经招到", "已经招满", "编制已满", "坑位已满", "已结束", "已截止", "职位已下", "岗位已撤" ]; const isRejection = rejectionPatterns.some(pattern => lastMessage.includes(pattern) ); if (isRejection) { this.log(`检测到HR拒绝,不生成建议: "${lastMessage.slice(0, 30)}..."`); return false; } const replyText = await this.generatePersonalizedReply(lastMessage); if (!replyText) return false; this.log(`AI建议回复: ${replyText.slice(0, 30)}...`); const inputBox = await this.waitForElement("#chat-input"); if (!inputBox) return false; inputBox.textContent = ""; inputBox.focus(); document.execCommand("insertText", false, replyText); this.log('建议回复已填入输入框,请审核后发送'); return true; } catch (error) { this.log(`生成建议回复出错: ${error.message}`); return false; } }, async _suggestDeclineForMismatch(positionName, includeKeywords) { try { const resumeText = state.settings.resumeText || ''; const declinePrompt = `你是一位求职者,收到了一个HR的打招呼消息,但这个岗位和你的专业方向不匹配。 岗位名称: ${positionName} 你的求职方向: ${includeKeywords.join('、')} 简历摘要: ${resumeText.slice(0, 500) || '未提供'} 请生成一句礼貌的婉拒回复(20-40字),表达: 1. 感谢对方的关注 2. 说明岗位和自己的专业方向不太匹配 3. 祝愿对方早日找到合适的人选 语气要真诚、礼貌,不要生硬。直接给出回复内容,不需要引号或前缀。`; const replyText = await this.requestAi(declinePrompt, '你是一位求职者,需要礼貌地婉拒不匹配的岗位。回复要真诚礼貌、简洁。'); if (!replyText) return false; this.log(`AI婉拒建议: ${replyText.slice(0, 30)}...`); const inputBox = await this.waitForElement("#chat-input"); if (!inputBox) return false; inputBox.textContent = ""; inputBox.focus(); document.execCommand("insertText", false, replyText); this.log('婉拒建议已填入输入框,请审核后发送'); return true; } catch (error) { this.log(`生成婉拒建议出错: ${error.message}`); return false; } }, async aiReply() { if (!state.isRunning) return; try { const autoReplyEnabled = JSON.parse( localStorage.getItem("autoReply") || "false" ); if (!autoReplyEnabled) { return false; } const lastMessage = await this.getLastFriendMessageText(); if (!lastMessage) return false; // 检测HR是否明确拒绝 const rejectionPatterns = [ "不合适", "不符合", "不需要", "暂时不招", "已招满", "谢谢", "再见", "不合适", "不符合条件", "不符合要求", "我们要求", "您的条件", "经验不足", "学历不够", "暂不匹配", "不匹配", "不太合适", "不是很合适", "暂时没有", "暂无需求", "没有需求", "不需要人", "已经招到", "已经招满", "编制已满", "坑位已满", "已结束", "已截止", "职位已下", "岗位已撤" ]; const isRejection = rejectionPatterns.some(pattern => lastMessage.includes(pattern) ); if (isRejection) { this.log(`检测到HR拒绝,停止对话: "${lastMessage.slice(0, 30)}..."`); // 关闭聊天窗口 const closeBtn = document.querySelector(".chat-header-close, .close-btn, [class*='close']"); if (closeBtn) { closeBtn.click(); this.log("已关闭拒绝的聊天窗口"); } return false; } // 免费版本:移除每日回复次数限制 // const today = new Date().toISOString().split("T")[0]; // if (state.ai.lastAiDate !== today) { // state.ai.replyCount = 0; // state.ai.lastAiDate = today; // StatePersistence.saveState(); // } // 使用个性化回复(基于简历) const aiReplyText = await this.generatePersonalizedReply(lastMessage); if (!aiReplyText) return false; this.log(`AI回复: ${aiReplyText.slice(0, 30)}...`); const inputBox = await this.waitForElement("#chat-input"); if (!inputBox) return false; inputBox.textContent = ""; inputBox.focus(); document.execCommand("insertText", false, aiReplyText); await this.delay(CONFIG.OPERATION_INTERVAL / 10); const sendButton = DOMCache.get(".btn-send"); if (sendButton) { await this.simulateClick(sendButton); } else { const enterKeyEvent = new KeyboardEvent("keydown", { key: "Enter", keyCode: 13, code: "Enter", which: 13, bubbles: true, }); inputBox.dispatchEvent(enterKeyEvent); } return true; } catch (error) { ErrorHandler.handle(error, 'Core.aiReply'); this.log(`AI回复出错: ${error.message}`); return false; } }, async requestAi(message, systemRoleOverride = null) { const apiKey = localStorage.getItem("aiApiKey") || ""; const apiUrl = localStorage.getItem("aiApiUrl") || ""; const model = localStorage.getItem("aiModel") || ""; const apiType = localStorage.getItem("aiApiType") || "openai"; const customRole = localStorage.getItem("aiRole"); const customHeaders = localStorage.getItem("aiCustomHeaders") || ""; const defaultSystemRole = "你是一个正在BOSS直聘上与HR聊天的求职者。回复要求:简短口语化(30字内),像朋友微信聊天;有经验说经验,没经验说能力;工资说范围、到岗给时间、离职说成长;不说您好、不模板化、不写小作文。"; const systemRole = systemRoleOverride || customRole || defaultSystemRole; if (!apiUrl || !model) { this.log("未配置 AI 接口,请先填写 API 地址和模型名称"); return null; } let requestConfig; try { requestConfig = buildAiApiRequest({ apiType, apiUrl, apiKey, model, systemRole, message, maxTokens: 512, customHeaders }); } catch (error) { this.log("AI 配置无效:" + error.message); return null; } // 单次请求 const doRequest = () => new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error("AI请求超时(60秒)")); }, 60000); GM_xmlhttpRequest({ method: "POST", url: requestConfig.url, headers: requestConfig.headers, data: JSON.stringify(requestConfig.data), timeout: 60000, onload: (response) => { clearTimeout(timeoutId); try { const result = JSON.parse(response.responseText || "{}"); if (response.status < 200 || response.status >= 300) { throw new Error("HTTP错误 " + response.status + ": " + (result?.error?.message || result?.message || "未知错误")); } resolve(extractAiResponse(result, apiType)); } catch (error) { reject( new Error( "响应解析失败: " + error.message + "\n原始响应: " + response.responseText ) ); } }, onerror: (error) => { clearTimeout(timeoutId); reject(new Error("网络请求失败: " + error)); }, ontimeout: () => { clearTimeout(timeoutId); reject(new Error("请求超时")); }, }); }); // 重试逻辑:失败后等3秒再试1次 try { return await doRequest(); } catch (firstErr) { this.log(`AI首次请求失败: ${firstErr.message},3秒后重试...`); await this.delay(3000); try { return await doRequest(); } catch (secondErr) { throw new Error(`AI请求失败(已重试): ${secondErr.message}`); } } }, // 读取简历文件内容 async readResumeFile(file) { return new Promise(async (resolve, reject) => { const reader = new FileReader(); reader.onload = async (e) => { try { const content = e.target.result; // 对于PDF文件,尝试提取文本 if (file.type === 'application/pdf' || file.name.endsWith('.pdf')) { // PDF文本提取需要额外处理(使用PDF.js) const text = await this.extractTextFromPDF(content.slice(0)); // 先读取 PDF 文本层;扫描件会自动尝试浏览器端 OCR。 if (!text || text.length < 20) { const ocrText = await this.extractTextFromPDFWithOCR(content); if (ocrText && ocrText.length >= 20) { resolve({ success: true, text: ocrText }); } else { resolve({ success: false, text: "", message: "未能从 PDF 中识别出有效文本。已尝试文本提取和 OCR;请确认 PDF 未加密,或将简历内容直接粘贴到文本框。" }); } } else { resolve({ success: true, text: text }); } } else if (file.name.endsWith('.doc') || file.name.endsWith('.docx')) { // Word文件尝试读取 const text = this.extractTextFromWord(content); if (!text || text.length < 50) { resolve({ success: false, text: "", message: "【Word提取失败】\n\n浏览器无法直接读取此Word文件的文本内容。\n\n【解决方案】\n请直接打开Word文件,按 Ctrl+A 全选,Ctrl+C 复制,\n然后粘贴到下方的简历内容文本框中。" }); } else { resolve({ success: true, text: text }); } } else { // 文本文件直接读取 resolve({ success: true, text: content }); } } catch (error) { reject(new Error("文件解析失败: " + error.message)); } }; reader.onerror = () => { reject(new Error("文件读取失败")); }; // 根据文件类型选择读取方式 if (file.type === 'application/pdf' || file.name.endsWith('.pdf') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) { reader.readAsArrayBuffer(file); } else { reader.readAsText(file, 'UTF-8'); } }); }, // 从Word文件提取文本(简单实现) extractTextFromWord(arrayBuffer) { try { // 将ArrayBuffer转换为Uint8Array const uint8Array = new Uint8Array(arrayBuffer); // 尝试解码为文本 const decoder = new TextDecoder('utf-8'); let text = decoder.decode(uint8Array); // 清理Word文档中的XML标签和特殊字符 text = text .replace(/<[^>]+>/g, ' ') // 移除XML标签 .replace(/\{[^}]+\}/g, ' ') // 移除Word特殊标记 .replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s\n\.,;:!?,。;:!?]/g, ' ') // 保留常用字符 .replace(/\s+/g, ' ') // 合并多个空格 .trim(); return text; } catch (error) { console.error("Word解析错误:", error); return ""; } }, // 扫描件没有文字层时,渲染前 4 页后在浏览器端进行 OCR。 async extractTextFromPDFWithOCR(arrayBuffer) { if (typeof pdfjsLib === "undefined" || typeof Tesseract === "undefined") return ""; let pdf; try { pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js"; pdf = await pdfjsLib.getDocument({ data: arrayBuffer.slice(0) }).promise; const maxPages = Math.min(pdf.numPages, 4); let fullText = ""; for (let i = 1; i <= maxPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 2 }); const canvas = document.createElement("canvas"); canvas.width = Math.ceil(viewport.width); canvas.height = Math.ceil(viewport.height); await page.render({ canvasContext: canvas.getContext("2d", { willReadFrequently: true }), viewport }).promise; this.log(`PDF 扫描件 OCR:正在识别第 ${i}/${maxPages} 页…`); const result = await Tesseract.recognize(canvas, "chi_sim+eng"); fullText += (result?.data?.text || "") + "\n"; page.cleanup(); } return fullText.replace(/\n{2,}/g, "\n").replace(/[ \t]+\n/g, "\n").trim(); } catch (error) { console.warn("PDF OCR 失败:", error); return ""; } finally { try { await pdf?.destroy(); } catch (_) {} } }, // 使用PDF.js提取PDF文本 async extractTextFromPDF(arrayBuffer) { let pdf; try { if (typeof pdfjsLib === "undefined") { console.error("PDF.js库未加载"); return ""; } pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js"; pdf = await pdfjsLib.getDocument({ data: arrayBuffer.slice(0) }).promise; this.log(`PDF 加载成功,共 ${pdf.numPages} 页,正在提取文本…`); let fullText = ""; for (let i = 1; i <= pdf.numPages; i++) { try { const page = await pdf.getPage(i); const textContent = await page.getTextContent(); const pageText = textContent.items .map(item => item.str) .filter(str => str && str.trim().length > 0) .join(" "); if (pageText) fullText += pageText + "\n"; page.cleanup(); } catch (pageError) { console.warn(`提取 PDF 第 ${i} 页文本失败:`, pageError); } } fullText = fullText.replace(/\s+/g, " ").replace(/\n+/g, "\n").trim(); return fullText.length >= 50 ? fullText : ""; } catch (error) { console.warn("PDF 文本提取失败:", error); return ""; } finally { try { await pdf?.destroy(); } catch (_) {} } }, // AI分析简历 // 截断简历文本到指定长度(避免超过AI token限制) truncateResumeText(resumeText, maxLength = 8000) { if (!resumeText || resumeText.length <= maxLength) { return resumeText; } // 尝试在句子边界截断 let truncated = resumeText.substring(0, maxLength); const lastPeriod = truncated.lastIndexOf('。'); const lastNewline = truncated.lastIndexOf('\n'); const lastSpace = truncated.lastIndexOf(' '); // 找到最后一个合适的截断点 const cutPoint = Math.max(lastPeriod, lastNewline, lastSpace); if (cutPoint > maxLength * 0.8) { truncated = truncated.substring(0, cutPoint + 1); } return truncated + "\n[简历内容已截断,仅显示前" + truncated.length + "字符]"; }, async analyzeResumeWithAI(resumeText) { try { this.log("正在使用AI分析简历..."); // 检查简历内容是否为空 if (!resumeText || resumeText.trim().length < 50) { throw new Error("简历内容太短或为空,请检查是否正确上传了简历文件"); } // 截断简历文本,避免超过token限制 const truncatedResume = this.truncateResumeText(resumeText, 6000); if (truncatedResume.length < resumeText.length) { this.log(`简历内容已截断: ${resumeText.length} -> ${truncatedResume.length} 字符`); } // 记录简历前200字符用于调试 this.log(`简历内容前200字符: ${truncatedResume.substring(0, 200)}...`); const analysisPrompt = `【重要】请基于以下提供的真实简历内容进行分析,不要生成示例或模板内容: ===简历开始=== ${truncatedResume} ===简历结束=== 请严格基于上述简历中的真实信息,分析并输出以下内容: 1. 核心技能(从简历中提取的具体技能,不要编造) 2. 工作经验亮点(基于简历中的工作经历) 3. 教育背景(简历中的真实教育信息) 4. 个人优势(基于简历内容总结) 5. 适合岗位类型(根据简历技能匹配) 【警告】如果简历内容为空或不清晰,请直接回复"简历内容无法识别,请检查上传的文件"。 不要生成示例数据!必须基于真实简历内容分析。`; const analysis = await this.requestAi(analysisPrompt); // 检查AI是否返回了示例内容(包含"某知名大学"、"某顶尖大学"等模板词汇) const templateKeywords = ['某知名大学', '某顶尖大学', '某大学', '示例', '模板', '某某']; const isTemplate = templateKeywords.some(keyword => analysis.includes(keyword)); if (isTemplate) { this.log("警告:AI返回了模板内容,可能未正确读取简历"); return "【警告】AI未能正确识别简历内容,请检查:\n1. 文件是否正确上传\n2. 简历内容是否可提取(PDF可能是扫描件)\n3. 尝试直接粘贴简历文本到文本框中\n\n原始返回内容:\n" + analysis; } this.log("简历分析完成"); return analysis; } catch (error) { this.log(`简历分析失败: ${error.message}`); throw error; } }, // 根据简历生成一条用于首次沟通的自我介绍 async generateGreetingsFromResume(resumeText) { try { this.log("正在根据简历生成自我介绍..."); const truncatedResume = this.truncateResumeText(resumeText, 4000); if (!truncatedResume || truncatedResume.trim().length < 20) { throw new Error("简历内容不足,无法生成自我介绍"); } const greetingPrompt = `仅根据以下简历内容,生成一条用于首次联系招聘者的自我介绍。\n\n【简历内容】\n${truncatedResume}\n\n【严格要求】\n1. 只能使用简历中明确出现的信息,不得补充、推测或虚构。\n2. 只输出一段完整的话,不要标题、编号、Markdown、引号或解释。\n3. 控制在 50—90 个汉字左右,简洁清晰地概括主要经验、技能或求职方向。\n4. 语气专业、真诚、有礼貌,并自然表达希望进一步沟通。\n5. 不要使用“我是一个”之类空泛表述,不要罗列过多技能。`; const response = await this.requestAi(greetingPrompt); const content = String(response || "") .replace(/^\s*```(?:text|markdown)?\s*/i, "") .replace(/\s*```\s*$/i, "") .replace(/^\s*(?:[((]?\d+[)).]|[-•])\s*/, "") .replace(/\s+/g, " ") .trim() .slice(0, 160); if (content.length < 10) { throw new Error("AI 返回的自我介绍内容过短"); } state.settings.greetingsList = [ { id: "resume-introduction", content }, ]; StatePersistence.saveState(); this.log("已生成 1 条自我介绍"); setTimeout(() => { if (typeof renderGreetingsList === "function") { renderGreetingsList(); } }, 0); return state.settings.greetingsList; } catch (error) { this.log(`生成自我介绍失败: ${error.message}`); return null; } }, // 根据简历信息生成个性化回复 async generatePersonalizedReply(hrMessage) { try { const resumeText = state.settings.resumeText || ""; const resumeAnalysis = state.settings.resumeAnalysis || ""; // 自动生成匹配的角色设定 const autoRole = await this.generateAutoRole(resumeText, resumeAnalysis); if (!resumeText && !resumeAnalysis) { // 没有简历信息,使用普通AI回复(但应用自动角色) return await this.requestAi(hrMessage, autoRole); } // 截断简历文本 const truncatedResume = this.truncateResumeText(resumeText, 3000); const replyPrompt = `【角色设定】 ${autoRole} 【简历信息】 ${truncatedResume} ${resumeAnalysis ? `【简历亮点分析】\n${resumeAnalysis}\n` : ''} 【HR的消息】"${hrMessage}" 【回复要求】 1. 回复要自然、口语化,像真人聊天,像朋友间交流 2. 不要用"您好""很高兴""我是"这种正式开场白 3. 结合简历中的实际经历和技能来回答,展示真实能力 4. 如果HR问技术问题,展示你的专业能力,用具体项目或经历佐证 5. 如果HR问薪资,给出一个合理范围,不要太死板 6. 如果HR问到岗时间,可以表达诚意但不要承诺太死 7. 回复控制在30字以内,要简洁有力 8. 可以用适当的语气词和标点来增加真实感 9. 不要暴露你是AI,措辞要像真人自然交流 直接给出回复内容,不需要任何前缀说明。`; const reply = await this.requestAi(replyPrompt); return reply; } catch (error) { this.log(`生成个性化回复失败: ${error.message}`); // 失败时使用普通AI回复 return await this.requestAi(hrMessage); } }, // 根据简历自动生成匹配的角色设定 async generateAutoRole(resumeText, resumeAnalysis) { // 基础角色设定 let baseRole = "你是一个在BOSS直聘上和HR聊天的求职者,回复简短口语化(30字内),像朋友微信聊天,不说套话不写小作文"; if (!resumeText && !resumeAnalysis) { return baseRole; } // 从简历分析中提取关键信息来判断角色类型 const textToAnalyze = (resumeText + " " + (resumeAnalysis || "")).toLowerCase(); // 检测是否为应届生 const isFreshGraduate = /应届|毕业|实习经历|无工作经历/.test(textToAnalyze) && !/1-3年|3-5年|5年|10年/.test(textToAnalyze); // 检测是否有工作经验 const yearsMatch = textToAnalyze.match(/(\d+)年/i); const years = yearsMatch ? parseInt(yearsMatch[1]) : 0; // 检测技术背景 const isTech = /开发|工程|技术|编程|代码|前端|后端|java|python|js|javascript|sql|数据库|算法/.test(textToAnalyze); // 检测管理背景 const isManagement = /管理|带领|团队|负责|主管|经理|总监/.test(textToAnalyze); // 检测设计背景 const isDesign = /设计|ui|ux|创意|审美|视觉/.test(textToAnalyze); // 根据检测结果生成角色 let roleDescription = ""; if (isFreshGraduate) { roleDescription = "你是一个刚毕业或即将毕业的应届生,对职场充满热情但不世故,说话真诚直接,有点青涩但很努力,会主动表达学习意愿"; } else if (years > 0 && years <= 3) { roleDescription = "你是一个有" + years + "年工作经验的职场新人,跳槽原因是追求更好的发展机会,说话比较实在,会主动展示自己的项目经验"; } else if (years > 3 && years <= 5) { roleDescription = "你是一个有" + years + "年工作经验的职场人,跳槽是为了职业发展,说话成熟稳重,会从专业角度回答问题"; } else if (years > 5) { roleDescription = "你是一个资深的职场专业人士,有" + years + "年丰富经验,说话专业自信,跳槽是为了更好的平台或转型,会强调自己的核心竞争力"; } else { roleDescription = "你是一个正在求职的职场人,说话自然诚恳,会主动展示自己的优势,坦诚回答薪资到岗等问题"; } // 根据技术背景添加特点 if (isTech) { roleDescription += "。对技术问题会用通俗易懂的方式解释"; } // 根据管理背景添加特点 if (isManagement) { roleDescription += "。有团队管理经验,会强调自己的协作和领导能力"; } // 根据设计背景添加特点 if (isDesign) { roleDescription += "。有设计思维,说话有审美感和创意"; } return baseRole + "。" + roleDescription + "。回复要简短,30字以内,像真人手机聊天一样自然,不要模板套话。"; }, async getLastFriendMessageText() { try { const chatContainer = DOMCache.get(".chat-message .im-list"); if (!chatContainer) return null; const friendMessages = Array.from( chatContainer.querySelectorAll("li.message-item.item-friend") ); if (friendMessages.length === 0) return null; const lastMessageEl = friendMessages[friendMessages.length - 1]; const textEl = lastMessageEl.querySelector(".text span"); return textEl?.textContent?.trim() || null; } catch (error) { ErrorHandler.handle(error, 'Core.getLastFriendMessageText'); this.log(`获取消息出错: ${error.message}`); return null; } }, async simulateClick(element) { if (!element) return; const rect = element.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; const dispatchMouseEvent = (type, options = {}) => { const event = new MouseEvent(type, { bubbles: true, cancelable: true, view: document.defaultView, clientX: x, clientY: y, ...options, }); element.dispatchEvent(event); }; dispatchMouseEvent("mouseover"); await this.delay(CONFIG.DELAYS.SHORT); dispatchMouseEvent("mousemove"); await this.delay(CONFIG.DELAYS.SHORT); dispatchMouseEvent("mousedown", { button: 0 }); await this.delay(CONFIG.DELAYS.SHORT); dispatchMouseEvent("mouseup", { button: 0 }); await this.delay(CONFIG.DELAYS.SHORT); dispatchMouseEvent("click", { button: 0 }); }, async waitForElement(selectorOrFunction, timeout = 5000) { return new Promise((resolve) => { let element; if (typeof selectorOrFunction === "function") element = selectorOrFunction(); else element = document.querySelector(selectorOrFunction); if (element) return resolve(element); const timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, timeout); const observer = new MutationObserver(() => { if (typeof selectorOrFunction === "function") element = selectorOrFunction(); else element = document.querySelector(selectorOrFunction); if (element) { clearTimeout(timeoutId); observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); }); }, getContextMultiplier(context) { const multipliers = { dict_load: 1.0, click: 0.8, selection: 0.8, default: 1.0, }; return multipliers[context] || multipliers["default"]; }, async smartDelay(baseTime, context = "default") { const multiplier = this.getContextMultiplier(context); const adjustedTime = baseTime * multiplier; return this.delay(adjustedTime); }, async delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }, async handleGreetSettingsPage() { try { localStorage.setItem(STORAGE.VISITED_GREET_SET, "true"); await this.delay(1000); const titleElement = document.querySelector("h3.title-wrap"); if (titleElement) { titleElement.textContent = "请务必打开 打招呼语功能"; titleElement.style.color = "red"; titleElement.style.fontWeight = "bold"; titleElement.style.fontSize = "18px"; } const possibleSelectors = [ "h4 .ui-switch", ".ui-switch", "span.ui-switch", "[class*='ui-switch']" ]; let switchElement = null; for (const selector of possibleSelectors) { switchElement = document.querySelector(selector); if (switchElement) { break; } } if (switchElement) { const isChecked = switchElement.classList.contains("ui-switch-checked"); if (!isChecked) { await this.simulateClick(switchElement); await this.delay(800); const newSwitchElement = document.querySelector(possibleSelectors.find(s => document.querySelector(s))); if (newSwitchElement && newSwitchElement.classList.contains("ui-switch-checked")) { UI.notify("招呼语功能已启用", "success"); } else { await this.simulateClick(switchElement); await this.delay(500); const finalSwitchElement = document.querySelector(possibleSelectors.find(s => document.querySelector(s))); if (finalSwitchElement && finalSwitchElement.classList.contains("ui-switch-checked")) { UI.notify("招呼语功能已启用", "success"); } else { UI.notify("请手动启用招呼语功能", "warning"); } } } else { UI.notify("招呼语功能已启用", "success"); } } else { const allSwitches = document.querySelectorAll("[class*='switch']"); allSwitches.forEach((el, index) => { this.log(`开关 ${index + 1}: ${el.className}, 文本: ${el.textContent?.trim()}`); }); } } catch (error) { ErrorHandler.handle(error, 'Core.handleGreetSettingsPage'); } }, extractTwoCharKeywords(text) { const keywords = []; const cleanedText = text.replace(/[\s,,.。::;;""''\[\]\(\)\{\}]/g, ""); for (let i = 0; i < cleanedText.length - 1; i++) { keywords.push(cleanedText.substring(i, i + 2)); } return keywords; }, async resetCycle() { // 检查是否配置了多个城市 const cities = state.locationKeywords.filter(kw => kw.trim() !== ''); if (cities.length > 1 && state.currentCityIndex < cities.length - 1) { // 切换到下一个城市 state.currentCityIndex = (state.currentCityIndex || 0) + 1; const nextCity = cities[state.currentCityIndex]; this.log(`当前城市职位投递完成,准备切换到: ${nextCity}`); // 切换城市 const switched = await this.switchCity(nextCity); if (switched) { // 重置状态,继续投递 state.currentIndex = 0; state.jobList = []; this.log(`已切换到 ${nextCity},继续投递...`); // 延迟后重新开始 await this.delay(3000); toggleProcess(); // 停止当前循环 await this.delay(1000); toggleProcess(); // 重新开始 return; } } // 所有城市都投递完成 toggleProcess(); this.log("所有城市岗位沟通完成,恭喜您即将找到理想工作!"); state.currentIndex = 0; state.currentCityIndex = 0; }, async switchCity(cityName) { try { // 1. 点击城市选择器 const citySelector = document.querySelector('.city-label') || document.querySelector('[class*="city"]') || document.querySelector('.filter-city'); if (!citySelector) { this.log("未找到城市选择器,尝试直接修改搜索框"); return await this.switchCityBySearch(cityName); } citySelector.click(); await this.delay(1000); // 2. 在弹出的城市列表中查找目标城市 const cityInput = document.querySelector('.city-search input') || document.querySelector('.filter-city-search input'); if (cityInput) { // 输入城市名搜索 cityInput.value = cityName; cityInput.dispatchEvent(new Event('input', { bubbles: true })); await this.delay(1000); } // 3. 点击目标城市 const cityItems = document.querySelectorAll('.city-item, .filter-city-item, [class*="city-list"] li'); for (const item of cityItems) { if (item.textContent.includes(cityName)) { item.click(); this.log(`已选择城市: ${cityName}`); await this.delay(2000); // 等待页面刷新 return true; } } // 如果没找到,尝试直接搜索 return await this.switchCityBySearch(cityName); } catch (error) { this.log(`切换城市失败: ${error.message}`); return false; } }, async switchCityBySearch(cityName) { try { // 通过搜索框切换城市 const searchInput = document.querySelector('.search-input') || document.querySelector('input[placeholder*="搜索"]') || document.querySelector('.ipt-search'); if (!searchInput) { this.log("未找到搜索框,无法切换城市"); return false; } // 清空并输入新的搜索条件 searchInput.value = `${state.includeKeywords[0] || ''} ${cityName}`; searchInput.dispatchEvent(new Event('input', { bubbles: true })); await this.delay(500); // 触发搜索 const searchBtn = document.querySelector('.search-btn') || document.querySelector('.btn-search') || document.querySelector('button[type="submit"]'); if (searchBtn) { searchBtn.click(); } else { // 按回车键搜索 searchInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', keyCode: 13 })); } this.log(`已通过搜索切换到: ${cityName}`); await this.delay(3000); // 等待搜索结果加载 return true; } catch (error) { this.log(`通过搜索切换城市失败: ${error.message}`); return false; } }, log(message) { const logEntry = `[${new Date().toLocaleTimeString()}] ${message}`; const logPanel = document.querySelector("#pro-log"); if (logPanel) { if (state.comments.isCommentMode) { return; } const logItem = document.createElement("div"); logItem.className = "log-item"; logItem.style.padding = "0px 8px"; logItem.textContent = logEntry; logPanel.appendChild(logItem); logPanel.scrollTop = logPanel.scrollHeight; } }, async getCurrentCompanyName() { try { let companyName = ""; let retries = 0; const maxRetries = 10; while (retries < maxRetries && !companyName) { const bossInfoAttr = document.querySelector(".boss-info-attr"); if (bossInfoAttr) { const text = bossInfoAttr.textContent.trim(); if (text) { const parts = text.split("·"); if (parts.length >= 1) { companyName = parts[0].trim(); if (companyName) { return companyName; } } } } retries++; if (retries < maxRetries) { await this.delay(200); } } return companyName; } catch (error) { console.log(`获取公司名失败: ${error.message}`); return ""; } }, async fetchCompanyComments() { return { success: true, data: { list: [], total: 0 }, message: "公司评论在线服务已禁用" }; }, async submitCompanyComment() { return { success: false, message: "公司评论在线服务已禁用" }; }, displayComments(comments, companyName) { const logPanel = document.querySelector("#pro-log"); if (!logPanel) return; logPanel.innerHTML = ""; logPanel.style.position = "relative"; logPanel.style.padding = "0"; logPanel.style.height = "260px"; logPanel.style.display = "flex"; logPanel.style.flexDirection = "column"; if (!companyName) { const noCompanyItem = document.createElement("div"); noCompanyItem.className = "comment-item"; noCompanyItem.style.cssText = "padding: 0px; border-bottom: 1px solid #e5e7eb; color: #6b7280; text-align: center;"; noCompanyItem.textContent = "未找到公司信息"; logPanel.appendChild(noCompanyItem); return; } const contentContainer = document.createElement("div"); contentContainer.className = "comment-content-container"; contentContainer.style.cssText = "flex: 1; overflow-y: auto; padding: 12px; scrollbar-width: thin; scrollbar-color: var(--primary-color) var(--secondary-color);"; const headerItem = document.createElement("div"); headerItem.className = "comment-header"; headerItem.style.cssText = "padding: 0px; border-radius: 0px; margin-bottom: 0px;"; headerItem.innerHTML = `
${companyName}
`; contentContainer.appendChild(headerItem); if (!comments || comments.length === 0) { const noCommentsItem = document.createElement("div"); noCommentsItem.className = "comment-item"; noCommentsItem.style.cssText = "padding: 12px; border-bottom: 1px solid #e5e7eb; color: #6b7280; text-align: center;"; noCommentsItem.textContent = "这家公司还没有评论哦,来评论一下吧!"; contentContainer.appendChild(noCommentsItem); } else { comments.forEach((comment, index) => { const commentItem = document.createElement("div"); commentItem.className = "comment-item"; commentItem.style.cssText = "padding: 12px; border-bottom: 1px solid #e5e7eb; margin-bottom: 8px; background: #ffffff; border-radius: 8px;"; const contentDiv = document.createElement("div"); contentDiv.style.cssText = "color: #374151; font-size: 13px; line-height: 1.6; margin-bottom: 6px; word-break: break-word;"; contentDiv.textContent = comment.content || comment.comment || comment; const metaDiv = document.createElement("div"); metaDiv.style.cssText = "font-size: 11px; color: #9ca3af; display: flex; justify-content: space-between;"; const timeText = comment.createdAt || comment.time || new Date().toLocaleString(); metaDiv.innerHTML = `${timeText}`; commentItem.appendChild(contentDiv); commentItem.appendChild(metaDiv); contentContainer.appendChild(commentItem); }); } logPanel.appendChild(contentContainer); const inputContainer = document.createElement("div"); inputContainer.className = "comment-input-container"; inputContainer.style.cssText = "flex-shrink: 0; padding: 12px; background: var(--secondary-color); border-top: 1px solid #e5e7eb; display: flex; gap: 8px; align-items: center;"; const input = document.createElement("input"); input.type = "text"; input.id = "comment-input"; input.placeholder = "说点什么呢..."; input.style.cssText = "flex: 1; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 13px; font-family: inherit; box-sizing: border-box; outline: none;"; input.onfocus = () => { input.style.borderColor = "var(--primary-color)"; }; input.onblur = () => { input.style.borderColor = "#d1d5db"; }; const submitBtn = document.createElement("button"); submitBtn.textContent = "发送"; submitBtn.style.cssText = "padding: 8px 16px; background: var(--primary-color); color: white; border: none; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap; transition: all 0.2s ease;"; submitBtn.onmouseenter = () => { submitBtn.style.opacity = "0.9"; }; submitBtn.onmouseleave = () => { submitBtn.style.opacity = "1"; }; submitBtn.onclick = async () => { const commentText = input.value.trim(); if (!commentText) { alert("请输入评论内容"); return; } submitBtn.disabled = true; submitBtn.textContent = "提交中..."; const result = await this.submitCompanyComment(companyName, commentText); if (result.success) { alert("评论提交成功!"); input.value = ""; // 评论功能已移除 } else { alert(result.message || "评论提交失败"); } submitBtn.disabled = false; submitBtn.textContent = "发送"; }; inputContainer.appendChild(input); inputContainer.appendChild(submitBtn); logPanel.appendChild(inputContainer); contentContainer.scrollTop = contentContainer.scrollHeight; }, async loadAndDisplayComments() { const companyName = await this.getCurrentCompanyName(); state.comments.currentCompanyName = companyName; state.comments.isCommentMode = true; if (state.comments.isLoading) return; state.comments.isLoading = true; const logPanel = document.querySelector("#pro-log"); if (logPanel) { logPanel.innerHTML = '
加载评论中...
'; } const result = await this.fetchCompanyComments(companyName); state.comments.isLoading = false; const comments = result.success && result.data ? result.data.records : []; state.comments.commentsList = comments; this.displayComments(comments, companyName); }, }; async function toggleProcess() { state.isRunning = !state.isRunning; if (state.isRunning) { Core.scrollStopRequested = false; state.comments.isCommentMode = false; state.jobList = []; state.currentCityIndex = 0; state.includeKeywords = elements.includeInput.value .trim() .toLowerCase() .split(/[,,、;;\n]/) .map((keyword) => keyword.trim()) .filter(Boolean); state.locationKeywords = (elements.locationInput?.value || "") .trim() .toLowerCase() .split(/[,,、;;\n]/) .map((keyword) => keyword.trim()) .filter(Boolean); elements.controlBtn.textContent = "停止海投"; elements.controlBtn.style.background = "#4285f4"; const logPanel = document.querySelector("#pro-log"); if (logPanel) { logPanel.innerHTML = ""; } const startTime = new Date(); Core.log(`开始自动海投,时间:${startTime.toLocaleTimeString()}`); Core.log( `筛选条件:职位名包含【${state.includeKeywords.join("、") || "无" }】,工作地包含【${state.locationKeywords.join("、") || "无"}】` ); // 如果有多个城市,先切换到第一个城市 const cities = state.locationKeywords.filter(kw => kw.trim() !== ''); if (cities.length > 0) { const firstCity = cities[0]; Core.log(`准备切换到第一个城市: ${firstCity}`); const switched = await Core.switchCity(firstCity); if (switched) { Core.log(`已切换到 ${firstCity},等待页面加载...`); await Core.delay(3000); } else { Core.log(`切换城市失败,使用当前页面`); } } Core.startProcessing(); } else { Core.cancelActiveScroll(); elements.controlBtn.textContent = "启动海投"; elements.controlBtn.style.background = "#4285f4"; state.isRunning = false; state.currentIndex = 0; // 评论功能已移除 } } function toggleChatProcess() { state.isRunning = !state.isRunning; if (state.isRunning) { elements.controlBtn.textContent = "停止智能对话"; elements.controlBtn.style.background = "#34a853"; const startTime = new Date(); Core.log(`开始智能对话,时间:${startTime.toLocaleTimeString()}`); Core.startProcessing(); } else { elements.controlBtn.textContent = "开启智能对话"; elements.controlBtn.style.background = "#34a853"; state.isRunning = false; if (Core.messageObserver) { Core.messageObserver.disconnect(); Core.messageObserver = null; } if (Core.chatListObserver) { Core.chatListObserver.disconnect(); Core.chatListObserver = null; } const stopTime = new Date(); Core.log(`停止智能对话,时间:${stopTime.toLocaleTimeString()}`); } } const STORAGE = { LETTER: "letterLastShown", GUIDE: "shouldShowGuide", AI_COUNT: "aiReplyCount", AI_DATE: "lastAiDate", VISITED_GREET_SET: "hasVisitedGreetSet", }; const letter = { showLetterToUser: function () { const COLORS = { primary: "#4285f4", text: "#333", textLight: "#666", background: "#f8f9fa", }; const overlay = document.createElement("div"); overlay.id = "letter-overlay"; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: flex; justify-content: center; align-items: center; z-index: 9999; backdrop-filter: blur(5px); animation: fadeIn 0.3s ease-out; `; const envelopeContainer = document.createElement("div"); envelopeContainer.id = "envelope-container"; envelopeContainer.style.cssText = ` position: relative; width: 90%; max-width: 650px; height: 400px; perspective: 1000px; `; const envelope = document.createElement("div"); envelope.id = "envelope"; envelope.style.cssText = ` position: absolute; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 0.6s ease; `; const envelopeBack = document.createElement("div"); envelopeBack.id = "envelope-back"; envelopeBack.style.cssText = ` position: absolute; width: 100%; height: 100%; background: ${COLORS.background}; border-radius: 10px; box-shadow: 0 15px 35px rgba(0,0,0,0.2); backface-visibility: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px; cursor: pointer; transition: all 0.3s; `; envelopeBack.innerHTML = `
${APP_ICONS.briefcase}求职助手
点击开启高效求职之旅
批量沟通 · 智能回复 · 简历解析 · 投递追踪
本地数据仅保存在当前浏览器
`; envelopeBack.addEventListener("click", () => { envelope.style.transform = "rotateY(180deg)"; setTimeout(() => { const content = document.getElementById("letter-content"); if (content) { content.style.display = "block"; content.style.animation = "fadeInUp 0.5s ease-out forwards"; } }, 300); }); const envelopeFront = document.createElement("div"); envelopeFront.id = "envelope-front"; envelopeFront.style.cssText = ` position: absolute; width: 100%; height: 100%; background: #fff; border-radius: 10px; box-shadow: 0 15px 35px rgba(0,0,0,0.2); transform: rotateY(180deg); backface-visibility: hidden; display: flex; flex-direction: column; `; const titleBar = document.createElement("div"); titleBar.style.cssText = ` padding: 20px 30px; background: #4285f4; color: white; font-size: clamp(1.2rem, 2.5vw, 1.4rem); font-weight: 600; border-radius: 10px 10px 0 0; display: flex; align-items: center; `; titleBar.innerHTML = `${APP_ICONS.briefcase}求职助手`; const letterContent = document.createElement("div"); letterContent.id = "letter-content"; letterContent.style.cssText = ` flex: 1; padding: 25px 30px; overflow-y: auto; font-size: clamp(0.95rem, 2vw, 1.05rem); line-height: 1.8; color: ${COLORS.text}; background-blend-mode: overlay; background-color: rgba(255,255,255,0.95); display: none; `; letterContent.innerHTML = `

你好,求职者:

欢迎使用求职助手?

本工具用于协助筛选职位、批量沟通、整理简历信息,并在当前浏览器中记录投递数据。

  • 智能岗位筛选,让投递更聚焦
  • 自动批量沟通,减少重复操作
  • 可配置 AI 回复,可接入兼容接口和中转站
  • 简历解析,支持 PDF 文本提取与扫描件 OCR
  • 投递数据中心,便于复盘与回看原始职位页

工具仅作效率辅助,请根据真实经历与岗位要求审慎沟通。祝你求职顺利!

`; const buttonArea = document.createElement("div"); buttonArea.style.cssText = ` padding: 15px 30px; display: flex; justify-content: center; border-top: 1px solid #eee; background: ${COLORS.background}; border-radius: 0 0 10px 10px; `; const startButton = document.createElement("button"); startButton.style.cssText = ` background: #4285f4; color: white; border: none; border-radius: 8px; padding: 12px 30px; font-size: clamp(1rem, 2vw, 1.1rem); font-weight: 500; cursor: pointer; transition: all 0.3s; box-shadow: 0 6px 16px rgba(66, 133, 244, 0.3); outline: none; display: flex; align-items: center; `; startButton.innerHTML = `${APP_ICONS.briefcase} 开始使用`; startButton.addEventListener("click", () => { const hasVisitedGreetSet = localStorage.getItem(STORAGE.VISITED_GREET_SET); if (!hasVisitedGreetSet) { localStorage.setItem(STORAGE.VISITED_GREET_SET, "true"); window.open( "https://www.zhipin.com/web/geek/notify-set?type=greetSet", "_blank" ); } envelopeContainer.style.animation = "scaleOut 0.3s ease-in forwards"; overlay.style.animation = "fadeOut 0.3s ease-in forwards"; setTimeout(() => { if (overlay.parentNode === document.body) { document.body.removeChild(overlay); } }, 300); }); buttonArea.appendChild(startButton); envelopeFront.appendChild(titleBar); envelopeFront.appendChild(letterContent); envelopeFront.appendChild(buttonArea); envelope.appendChild(envelopeBack); envelope.appendChild(envelopeFront); envelopeContainer.appendChild(envelope); overlay.appendChild(envelopeContainer); document.body.appendChild(overlay); const style = document.createElement("style"); style.textContent = ` @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } } @keyframes fadeOut { from { opacity: 1 } to { opacity: 0 } } @keyframes scaleOut { from { transform: scale(1); opacity: 1 } to { transform: scale(.9); opacity: 0 } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px) } to { opacity: 1; transform: translateY(0) } } #envelope-back:hover { transform: scale(1.02); box-shadow: 0 20px 40px rgba(0,0,0,0.25); } #envelope-front button:hover { transform: scale(1.05); box-shadow: 0 8px 20px rgba(66, 133, 244, 0.4); } #envelope-front button:active { transform: scale(0.98); } @media (max-width: 480px) { #envelope-container { height: 350px; } #letter-content { font-size: 0.9rem; padding: 15px; } } `; document.head.appendChild(style); }, }; const guide = { steps: [ { target: "div.city-label.active", content: '海投前,先在BOSS筛选出岗位!\n\n助手会先滚动收集界面上显示的岗位,\n随后依次进行沟通~', arrowPosition: "bottom", defaultPosition: { left: "50%", top: "20%", transform: "translateX(-50%)", }, }, { target: 'a[ka="header-jobs"]', content: '职位页操作流程:\n\n1. 扫描职位卡片\n2. 点击"立即沟通"(需开启"自动打招呼")\n3. 留在当前页,继续沟通下一个职位\n\n全程无需手动干预,高效投递!', arrowPosition: "bottom", defaultPosition: { left: "25%", top: "80px" }, }, { target: 'a[ka="header-message"]', content: '海投建议!\n\n• HR与您沟通,HR需要付费给平台\n因此您尽可能先自我介绍以提高效率 \n\n• HR查看附件简历,HR也要付费给平台\n所以尽量先发送`图片简历`给HR', arrowPosition: "left", defaultPosition: { right: "150px", top: "100px" }, }, { target: "div.logo", content: '您需要打开两个浏览器窗口:\n\n左侧窗口自动打招呼发起沟通\n右侧发送自我介绍和图片简历\n\n您只需专注于挑选offer!', arrowPosition: "right", defaultPosition: { left: "200px", top: "20px" }, }, { target: "div.logo", content: '特别注意:\n\n1. BOSS直聘每日打招呼上限为150次\n2. 聊天页仅处理最上方的最新对话\n3. 打招呼后对方会显示在聊天页\n4. 投递操作过于频繁有封号风险!', arrowPosition: "bottom", defaultPosition: { left: "50px", top: "80px" }, }, ], currentStep: 0, guideElement: null, overlay: null, highlightElements: [], showGuideToUser() { this.overlay = document.createElement("div"); this.overlay.id = "guide-overlay"; this.overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(2px); z-index: 99997; pointer-events: none; opacity: 0; transition: opacity 0.3s ease; `; document.body.appendChild(this.overlay); this.guideElement = document.createElement("div"); this.guideElement.id = "guide-tooltip"; this.guideElement.style.cssText = ` position: fixed; z-index: 99999; width: 320px; background: white; border-radius: 12px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; overflow: hidden; opacity: 0; transform: translateY(10px); transition: opacity 0.3s ease, transform 0.3s ease; `; document.body.appendChild(this.guideElement); setTimeout(() => { this.overlay.style.opacity = "1"; setTimeout(() => { this.showStep(0); }, 300); }, 100); }, showStep(stepIndex) { const step = this.steps[stepIndex]; if (!step) return; this.clearHighlights(); const target = document.querySelector(step.target); if (target) { const rect = target.getBoundingClientRect(); const highlight = document.createElement("div"); highlight.className = "guide-highlight"; highlight.style.cssText = ` position: fixed; top: ${rect.top}px; left: ${rect.left}px; width: ${rect.width}px; height: ${rect.height}px; background: ${step.highlightColor || "#4285f4"}; opacity: 0.2; border-radius: 4px; z-index: 99998; box-shadow: 0 0 0 4px ${step.highlightColor || "#4285f4"}; animation: guide-pulse 2s infinite; `; document.body.appendChild(highlight); this.highlightElements.push(highlight); this.setGuidePositionFromTarget(step, rect); } else { console.warn("引导目标元素未找到,使用默认位置:", step.target); this.setGuidePositionFromDefault(step); } let buttonsHtml = ""; if (stepIndex === this.steps.length - 1) { buttonsHtml = `
`; } else { buttonsHtml = `
`; } this.guideElement.innerHTML = `
求职助手使用引导
步骤 ${stepIndex + 1 }/${this.steps.length}
${step.content }
${buttonsHtml} `; if (stepIndex === this.steps.length - 1) { document .getElementById("guide-finish-btn") .addEventListener("click", () => this.endGuide(true)); } else { document .getElementById("guide-next-btn") .addEventListener("click", () => this.nextStep()); document .getElementById("guide-skip-btn") .addEventListener("click", () => this.endGuide()); } if (stepIndex === this.steps.length - 1) { const finishBtn = document.getElementById("guide-finish-btn"); finishBtn.addEventListener("mouseenter", () => { finishBtn.style.background = this.darkenColor( step.highlightColor || "#4285f4", 15 ); finishBtn.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)"; }); finishBtn.addEventListener("mouseleave", () => { finishBtn.style.background = step.highlightColor || "#4285f4"; finishBtn.style.boxShadow = "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)"; }); } else { const nextBtn = document.getElementById("guide-next-btn"); const skipBtn = document.getElementById("guide-skip-btn"); nextBtn.addEventListener("mouseenter", () => { nextBtn.style.background = this.darkenColor( step.highlightColor || "#4285f4", 15 ); nextBtn.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)"; }); nextBtn.addEventListener("mouseleave", () => { nextBtn.style.background = step.highlightColor || "#4285f4"; nextBtn.style.boxShadow = "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)"; }); skipBtn.addEventListener("mouseenter", () => { skipBtn.style.background = "#f3f4f6"; }); skipBtn.addEventListener("mouseleave", () => { skipBtn.style.background = "white"; }); } this.guideElement.style.opacity = "1"; this.guideElement.style.transform = "translateY(0)"; }, setGuidePositionFromTarget(step, rect) { let left, top; const guideWidth = 320; const guideHeight = 240; switch (step.arrowPosition) { case "top": left = rect.left + rect.width / 2 - guideWidth / 2; top = rect.top - guideHeight - 20; break; case "bottom": left = rect.left + rect.width / 2 - guideWidth / 2; top = rect.bottom + 20; break; case "left": left = rect.left - guideWidth - 20; top = rect.top + rect.height / 2 - guideHeight / 2; break; case "right": left = rect.right + 20; top = rect.top + rect.height / 2 - guideHeight / 2; break; default: left = rect.right + 20; top = rect.top; } left = Math.max(10, Math.min(left, window.innerWidth - guideWidth - 10)); top = Math.max(10, Math.min(top, window.innerHeight - guideHeight - 10)); this.guideElement.style.left = `${left}px`; this.guideElement.style.top = `${top}px`; this.guideElement.style.transform = "translateY(0)"; }, setGuidePositionFromDefault(step) { const position = step.defaultPosition || { left: "50%", top: "50%", transform: "translate(-50%, -50%)", }; Object.assign(this.guideElement.style, { left: position.left, top: position.top, right: position.right || "auto", bottom: position.bottom || "auto", transform: position.transform || "none", }); }, nextStep() { const currentStep = this.steps[this.currentStep]; if (currentStep) { const target = document.querySelector(currentStep.target); if (target) { target.removeEventListener("click", this.nextStep); } } this.currentStep++; if (this.currentStep < this.steps.length) { this.guideElement.style.opacity = "0"; this.guideElement.style.transform = "translateY(10px)"; setTimeout(() => { this.showStep(this.currentStep); }, 300); } }, clearHighlights() { this.highlightElements.forEach((el) => el.remove()); this.highlightElements = []; }, endGuide(isCompleted = false) { this.clearHighlights(); this.guideElement.style.opacity = "0"; this.guideElement.style.transform = "translateY(10px)"; this.overlay.style.opacity = "0"; setTimeout(() => { if (this.overlay && this.overlay.parentNode) { this.overlay.parentNode.removeChild(this.overlay); } if (this.guideElement && this.guideElement.parentNode) { this.guideElement.parentNode.removeChild(this.guideElement); } if (isCompleted && this.chatUrl) { window.open(this.chatUrl, "_blank"); } }, 300); document.dispatchEvent(new Event("guideEnd")); }, darkenColor(color, percent) { let R = parseInt(color.substring(1, 3), 16); let G = parseInt(color.substring(3, 5), 16); let B = parseInt(color.substring(5, 7), 16); R = parseInt((R * (100 - percent)) / 100); G = parseInt((G * (100 - percent)) / 100); B = parseInt((B * (100 - percent)) / 100); R = R < 255 ? R : 255; G = G < 255 ? G : 255; B = B < 255 ? B : 255; R = Math.round(R); G = Math.round(G); B = Math.round(B); const RR = R.toString(16).length === 1 ? "0" + R.toString(16) : R.toString(16); const GG = G.toString(16).length === 1 ? "0" + G.toString(16) : G.toString(16); const BB = B.toString(16).length === 1 ? "0" + B.toString(16) : B.toString(16); return `#${RR}${GG}${BB}`; }, }; const style = document.createElement("style"); style.textContent = ` @keyframes guide-pulse { 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(66, 133, 244, 0.4); } 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(66, 133, 244, 0); } 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(66, 133, 244, 0); } } .guide-content .highlight { font-weight: 700; color: #1a73e8; } .guide-content .warning { font-weight: 700; color: #d93025; } `; document.head.appendChild(style); function getToday() { return new Date().toISOString().split("T")[0]; } async function init() { try { const midnight = new Date(); midnight.setDate(midnight.getDate() + 1); midnight.setHours(0, 0, 0, 0); setTimeout(() => { localStorage.removeItem(STORAGE.AI_COUNT); localStorage.removeItem(STORAGE.AI_DATE); localStorage.removeItem(STORAGE.LETTER); }, midnight - Date.now()); UI.init(); document.body.style.position = "relative"; const today = getToday(); if (location.pathname.includes("/jobs")) { if (localStorage.getItem(STORAGE.LETTER) !== today) { letter.showLetterToUser(); localStorage.setItem(STORAGE.LETTER, today); } else if (localStorage.getItem(STORAGE.GUIDE) !== "true") { guide.showGuideToUser(); localStorage.setItem(STORAGE.GUIDE, "true"); } Core.log("求职助手已就绪:请先确认筛选条件和投递方式,再开始批量沟通。"); } else if (location.pathname.includes("/chat")) { Core.log("智能对话已就绪:默认只处理新消息,仅在开启自动发送简历并满足条件时才会发送。"); } else if (location.pathname.includes("/notify-set")) { Core.log("招呼语设置已就绪:将仅使用您保存的自我介绍内容。"); Core.handleGreetSettingsPage(); } else { Core.log("当前页面暂不支持,请移步至职位页面!"); } } catch (error) { console.error("初始化失败:", error); if (UI.notify) UI.notify("初始化失败", "error"); } } window.addEventListener("load", init); let lastUrl = location.href; new MutationObserver(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; // 评论功能已移除 } }).observe(document, { subtree: true, childList: true }); function normalizeGreetingsList() { const previous = Array.isArray(state.settings.greetingsList) ? state.settings.greetingsList : []; const first = previous.find((item) => item?.content?.trim()); const normalized = first ? [{ id: first.id || "resume-introduction", content: String(first.content).trim(), }] : []; const changed = JSON.stringify(previous) !== JSON.stringify(normalized); state.settings.greetingsList = normalized; if (changed) StatePersistence.saveState(); return normalized[0] || null; } // 兼容旧调用:始终只保留一条自我介绍。 function addGreetingItem() { if (!state.settings.greetingsList?.length) { state.settings.greetingsList = [{ id: "resume-introduction", content: "", }]; StatePersistence.saveState(); } renderGreetingsList(); } function renderGreetingsList() { const greetingsList = document.getElementById("greetings-list"); if (!greetingsList) return; const greeting = normalizeGreetingsList(); greetingsList.innerHTML = ""; const label = document.createElement("div"); label.textContent = "首次沟通自我介绍"; label.style.cssText = "color:#1e3a5f;font-size:13px;font-weight:700;margin-bottom:7px;"; const input = document.createElement("textarea"); input.className = "greeting-input"; input.rows = 3; input.maxLength = 160; input.placeholder = "AI 分析简历后会在这里生成一条自我介绍;也可以手动填写。"; input.value = greeting?.content || ""; input.style.cssText = "width:100%;min-height:76px;resize:vertical;box-sizing:border-box;padding:9px 10px;border:1px solid #cbd5e1;border-radius:8px;outline:none;background:#fff;color:#1f2937;font-size:13px;line-height:1.55;font-family:inherit;"; const hint = document.createElement("div"); hint.textContent = "发送时仅使用这一条内容。"; hint.style.cssText = "margin-top:7px;color:#64748b;font-size:12px;"; input.addEventListener("focus", () => { input.style.borderColor = "#3b82f6"; input.style.boxShadow = "0 0 0 3px rgba(59, 130, 246, 0.12)"; }); input.addEventListener("blur", () => { input.style.borderColor = "#cbd5e1"; input.style.boxShadow = "none"; }); input.addEventListener("input", (event) => { const content = event.target.value.slice(0, 160); state.settings.greetingsList = content.trim() ? [{ id: greeting?.id || "resume-introduction", content }] : []; StatePersistence.saveState(); }); greetingsList.append(label, input, hint); } function attachGreetingEventListeners() { // 事件已在单条文本框创建时绑定。 } function loadGreetings() { normalizeGreetingsList(); renderGreetingsList(); } function loadSettingsIntoUI() { const aiRoleInput = document.getElementById("ai-role-input"); if (aiRoleInput) { aiRoleInput.value = settings.ai.role; } const autoReplyInput = document.querySelector( "#toggle-auto-reply-mode input" ); if (autoReplyInput) { autoReplyInput.checked = settings.autoReply; } const autoSendResumeInput = document.querySelector( "#toggle-auto-send-resume input" ); if (autoSendResumeInput) { autoSendResumeInput.checked = settings.useAutoSendResume; } const excludeHeadhuntersInput = document.querySelector( "#toggle-exclude-headhunters input" ); if (excludeHeadhuntersInput) { excludeHeadhuntersInput.checked = settings.excludeHeadhunters; } const autoSendImageResumeInput = document.querySelector( "#toggle-auto-send-image-resume input" ); if (autoSendImageResumeInput) { autoSendImageResumeInput.checked = settings.useAutoSendImageResume; } loadGreetings(); } // 数据看板 - 存储读写 function syncRecordsToStorage(records) { try { GM_setValue('applicationRecords', JSON.stringify(records)); state.applicationRecords = records; } catch(e) { StorageManager.setItem("applicationRecords", records); } } function getRecordsFromStorage() { try { var v = GM_getValue('applicationRecords'); return v ? JSON.parse(v) : (StorageManager.getParsedItem("applicationRecords", [])); } catch(e) { return StorageManager.getParsedItem("applicationRecords", []); } } // ==================== 数据看板 - 页面内独立面板 ==================== function toggleInlineDashboard() { showDashInPage(); } var dashState = { panel: null, charts: {}, allRecords: [], filteredRecords: [], currentPage: 1, pageSize: 12, selectedJobs: [] }; var statMap = { applied: { text: "已投递", color: "#1976d2", bg: "#e6f2ff" }, replied: { text: "已回复", color: "#16803f", bg: "#e7f8ee" }, interview: { text: "面试中", color: "#a46000", bg: "#fff4d8" }, rejected: { text: "已拒绝", color: "#c03d36", bg: "#ffebe9" }, pending: { text: "待回复", color: "#5a6d86", bg: "#edf2f7" } }; function dashEsc(value) { const node = document.createElement("div"); node.textContent = value || ""; return node.innerHTML; } function dashDate(value) { const date = new Date(value); return value && !Number.isNaN(date.getTime()) ? date.toLocaleString("zh-CN", { hour12: false }) : "-"; } function dashSource(record) { if (record.sourceUrl && /^https?:\/\//i.test(record.sourceUrl)) return record.sourceUrl; const query = [record.jobName, record.companyName].filter(Boolean).join(" "); return `https://www.zhipin.com/web/geek/job?query=${encodeURIComponent(query)}`; } function showDashInPage() { if (dashState.panel) { dashState.panel.hidden = false; refreshDash(); return; } const panel = document.createElement("section"); panel.id = "jobflow-dashboard"; panel.style.cssText = "position:fixed;right:26px;top:72px;z-index:2147483646;width:min(1160px,calc(100vw - 40px));height:min(790px,calc(100vh - 96px));overflow:auto;border:1px solid #d7e5f2;border-radius:22px;background:#f6faff;color:#233b5a;box-shadow:0 24px 70px rgba(31,67,108,.22);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;"; panel.innerHTML = `

投递数据中心

独立浮动面板 · 本地数据 · 可回看职位原始页面

近 7 天投递趋势按投递时间
状态分布当前记录
投递记录
公司 / 岗位地区HR状态投递时间操作
`; document.body.appendChild(panel); dashState.panel = panel; panel.querySelector("#jf-close").onclick = () => { panel.hidden = true; destroyDashCharts(); }; panel.querySelector("#jf-refresh").onclick = refreshDash; panel.querySelector("#jf-export").onclick = exportDashCSV; panel.querySelector("#jf-clear").onclick = clearDashData; panel.querySelector("#jf-search").oninput = () => { filterDash(); renderDashTable(); }; panel.querySelector("#jf-filter").onchange = () => { filterDash(); renderDashTable(); }; panel.querySelector("#jf-body").onclick = event => { const open = event.target.closest("[data-open]"); const remove = event.target.closest("[data-delete]"); if (open) { const record = dashState.allRecords.find(item => String(item.id) === open.dataset.open); window.open(dashSource(record || {}), "_blank", "noopener"); } if (remove) deleteDashRecord(remove.dataset.delete); }; panel.querySelector("#jf-page").onclick = event => { const button = event.target.closest("[data-page]"); if (button && !button.disabled) { dashState.currentPage = Number(button.dataset.page); renderDashTable(); } }; refreshDash(); } function destroyDashCharts() { Object.values(dashState.charts).forEach(chart => { try { chart.destroy(); } catch (_) {} }); dashState.charts = {}; } function refreshDash() { dashState.allRecords = getRecordsFromStorage(); dashState.currentPage = 1; renderDashJobs(); filterDash(); renderDashStats(); renderDashCharts(); renderDashTable(); } function filterDash() { const keyword = (document.getElementById("jf-search")?.value || "").trim().toLowerCase(); const status = document.getElementById("jf-filter")?.value || ""; dashState.filteredRecords = dashState.allRecords.filter(record => { const all = [record.companyName, record.jobName, record.location, record.hrPosition].join(" ").toLowerCase(); return (!keyword || all.includes(keyword)) && (!status || record.status === status) && (!dashState.selectedJobs.length || dashState.selectedJobs.includes(record.jobName)); }); } function renderDashJobs() { const holder = document.getElementById("jf-jobs"); if (!holder) return; holder.innerHTML = ""; const count = {}; dashState.allRecords.forEach(record => { if (record.jobName) count[record.jobName] = (count[record.jobName] || 0) + 1; }); Object.entries(count).sort((a,b) => b[1] - a[1]).slice(0, 12).forEach(([name, number]) => { const selected = dashState.selectedJobs.includes(name); const button = document.createElement("button"); button.className = "jf-btn"; button.textContent = `${name} (${number})`; button.style.cssText = `padding:5px 9px;border-radius:999px;${selected ? "color:#1474dc;border-color:#75b8ff;background:#eaf4ff" : ""}`; button.onclick = () => { const index = dashState.selectedJobs.indexOf(name); if (index >= 0) dashState.selectedJobs.splice(index,1); else dashState.selectedJobs.push(name); filterDash(); renderDashJobs(); renderDashTable(); }; holder.appendChild(button); }); if (dashState.selectedJobs.length) { const clear = document.createElement("button"); clear.className = "jf-btn jf-danger"; clear.textContent = "清除筛选"; clear.style.cssText = "padding:5px 9px;border-radius:999px;"; clear.onclick = () => { dashState.selectedJobs = []; filterDash(); renderDashJobs(); renderDashTable(); }; holder.appendChild(clear); } } function renderDashStats() { const holder = document.getElementById("jf-stats"); if (!holder) return; const records = dashState.allRecords; const today = new Date(); today.setHours(0,0,0,0); const data = [["累计投递",records.length,"#1b78e9"],["今日投递",records.filter(r => new Date(r.applyTime) >= today).length,"#7656d8"],["收到回复",records.filter(r => ["replied","interview"].includes(r.status)).length,"#16803f"],["面试进度",records.filter(r => r.status === "interview").length,"#ae6900"]]; holder.innerHTML = data.map(([name,value,color]) => `
${name}
${value}
`).join(""); } function renderDashCharts() { if (typeof Chart === "undefined") return; destroyDashCharts(); const records = dashState.allRecords; const labels = [], values = []; for (let offset=6; offset>=0; offset--) { const day = new Date(); day.setHours(0,0,0,0); day.setDate(day.getDate()-offset); const next = new Date(day); next.setDate(next.getDate()+1); labels.push(`${day.getMonth()+1}/${day.getDate()}`); values.push(records.filter(r => r.applyTime >= day.getTime() && r.applyTime < next.getTime()).length); } dashState.charts.trend = new Chart(document.getElementById("jf-trend"), { type:"line", data:{ labels, datasets:[{ data:values, borderColor:"#2185ed", backgroundColor:"rgba(33,133,237,.12)", fill:true, tension:.36, pointRadius:3, pointBackgroundColor:"#2185ed" }] }, options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}}, scales:{x:{grid:{display:false},ticks:{color:"#7892ae"}},y:{beginAtZero:true,ticks:{precision:0,color:"#7892ae"},grid:{color:"#edf3f8"}}} } }); const keys = Object.keys(statMap), nums = keys.map(key => records.filter(r => (r.status || "applied") === key).length); dashState.charts.status = new Chart(document.getElementById("jf-status"), { type:"doughnut", data:{labels:keys.map(key=>statMap[key].text),datasets:[{data:nums,backgroundColor:["#4aa2ff","#43ba7c","#f4be51","#ef7e75","#aebdcd"],borderWidth:0}]},options:{responsive:true,maintainAspectRatio:false,cutout:"68%",plugins:{legend:{position:"bottom",labels:{boxWidth:9,usePointStyle:true,font:{size:11},color:"#637b96"}}}} }); } function renderDashTable() { const body = document.getElementById("jf-body"), count = document.getElementById("jf-count"); if (!body) return; const records = dashState.filteredRecords; const pages = Math.max(1,Math.ceil(records.length/dashState.pageSize)); dashState.currentPage = Math.min(dashState.currentPage,pages); const current = records.slice((dashState.currentPage-1)*dashState.pageSize,dashState.currentPage*dashState.pageSize); count.textContent = `共 ${records.length} 条`; body.innerHTML = current.length ? current.map(record => { const state = statMap[record.status] || statMap.applied; const source = record.sourceUrl ? "查看原页" : "搜索岗位"; return `
${dashEsc(record.companyName||"-")}
${dashEsc(record.jobName||"-")}
${dashEsc(record.location||"-")}${dashEsc(record.hrPosition||"-")}${state.text}${dashDate(record.applyTime)}`; }).join("") : `${dashState.allRecords.length ? "没有符合筛选条件的记录" : "暂无投递记录,开始沟通职位后会在这里显示"}`; const page = document.getElementById("jf-page"); if (pages === 1) { page.innerHTML = ""; return; } let html = ``; for(let number=1;number<=pages;number++){if(number>6&&number1)continue;html+=``;} page.innerHTML=html+``; } function exportDashCSV() { const records = dashState.filteredRecords.length ? dashState.filteredRecords : dashState.allRecords; const header=["公司","岗位","地区","HR","状态","投递时间","职位原始页"]; const rows=records.map(r=>[r.companyName||"",r.jobName||"",r.location||"",r.hrPosition||"",(statMap[r.status]||statMap.applied).text,dashDate(r.applyTime),r.sourceUrl||""]); const csv=[header,...rows].map(row=>row.map(cell=>`"${String(cell).replace(/"/g,'""')}"`).join(",")).join("\n"); const link=document.createElement("a"); link.href=URL.createObjectURL(new Blob(["\ufeff"+csv],{type:"text/csv;charset=utf-8"})); link.download=`投递记录_${new Date().toISOString().slice(0,10)}.csv`; link.click(); setTimeout(()=>URL.revokeObjectURL(link.href),0); } function clearDashData() { if(!confirm("确定清空所有投递记录?此操作不可恢复。"))return; try{GM_setValue("applicationRecords",JSON.stringify([]));GM_setValue("lastClearedAt",JSON.stringify(Date.now()));}catch(_){StorageManager.setItem("applicationRecords",[]);} state.applicationRecords=[];refreshDash(); } function deleteDashRecord(id) { if(!confirm("确定删除这条记录?"))return; const records=getRecordsFromStorage().filter(record=>String(record.id)!==String(id)); try{GM_setValue("applicationRecords",JSON.stringify(records));}catch(_){StorageManager.setItem("applicationRecords",records);} state.applicationRecords=records;refreshDash(); } })();