From 94a58ea362f681679e6b36cc7d90e5276a4c8c21 Mon Sep 17 00:00:00 2001 From: xuxin <15279969124@163.com> Date: Fri, 7 Aug 2026 10:16:44 +0800 Subject: [PATCH] =?UTF-8?q?hotjob=E6=97=B6=E9=97=B4=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=E5=92=8C=E6=90=9C=E7=B4=A2=E4=B8=8B=E6=8B=89=E5=9E=8B?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A1=86=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SidebarPanel.tsx | 4 +- src/handlers/handleAutoFillHotjob.ts | 198 ++++++++++++++++++++++++--- src/lib/datePicker.ts | 39 +++++- src/lib/formStyle.ts | 4 +- src/lib/pickerFill.ts | 9 +- 5 files changed, 224 insertions(+), 30 deletions(-) diff --git a/src/components/SidebarPanel.tsx b/src/components/SidebarPanel.tsx index 440c48b..2217a26 100644 --- a/src/components/SidebarPanel.tsx +++ b/src/components/SidebarPanel.tsx @@ -993,7 +993,7 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
登录后将自动同步你的会员状态、简历和求职信息
@@ -1053,7 +1053,7 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps) - + {jobInfo.matchScore}% diff --git a/src/handlers/handleAutoFillHotjob.ts b/src/handlers/handleAutoFillHotjob.ts index 642dfb1..c101be9 100644 --- a/src/handlers/handleAutoFillHotjob.ts +++ b/src/handlers/handleAutoFillHotjob.ts @@ -7,8 +7,9 @@ * 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。 */ -import { fillMatchedField, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill" +import { fillMatchedField, isSearchPickerField, fillSearchPickerField, forceSetValue, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill" import { delay } from "~utils/delay" +import { simulateClick } from "~utils/domEvent" import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom" import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher" import { detectPickerField } from "~lib/pickerDetector" @@ -23,6 +24,168 @@ import { setFieldHighlight, isRequiredField } from "~lib/formStyle" import { get as storageGet, set as storageSet } from "~utils/storage" import { findLabelForInput } from "~lib/labelFinder" +/** Hotjob平台需要走全DOM搜索ant-select选项的字段标签名(这些字段弹出层在页面DOM末尾,通用逻辑定位不到) */ +const HOTJOB_ANT_SELECT_LABELS = ["学校"] + +/** Hotjob平台时间字段标签名(Ant DatePicker弹出层在DOM末尾,需要手动定位ant-calendar-picker-container) */ +const HOTJOB_DATE_LABELS = ["开始时间", "结束时间"] + +/** + * Hotjob平台特殊处理:全DOM搜索 ant-select-dropdown-menu-item 选项并点击 + * 原因:Hotjob的Ant Select弹出层渲染在页面DOM最末尾,不在input附近, + * 通用的弹出层检测逻辑(DOM差异对比)会误匹配到input内部的镜像span元素 + * @param inputEl - 搜索输入框元素 + * @param fillValue - 要填写的值(如学校名称) + * @returns 是否成功选中选项 + */ +async function hotjobFillAntSelectField( + inputEl: HTMLInputElement | HTMLTextAreaElement, + fillValue: string +): Promise { + if (!inputEl || !fillValue) return false + + console.log(`OfferPie: [Hotjob-AntSelect] "${fillValue}" 开始填写`) + + // 1. 点击input获取焦点并写入搜索值 + inputEl.focus() + simulateClick(inputEl as HTMLElement) + await delay("mid") + forceSetValue(inputEl, fillValue) + + // 2. 等待接口返回数据(Hotjob接口较慢,用max延时) + await delay("max") + + // 3. 全DOM搜索 ant-select-dropdown-menu-item,文字匹配目标值 + const allItems = document.querySelectorAll("li.ant-select-dropdown-menu-item") + let targetItem: HTMLElement | null = null + for (const item of Array.from(allItems)) { + const text = item.textContent?.trim() + if (text && text === fillValue) { + targetItem = item as HTMLElement + break + } + } + + if (!targetItem) { + console.log(`OfferPie: ❌ [Hotjob-AntSelect] 未找到匹配选项 "${fillValue}"`) + return false + } + + // 4. 用simulateClick点击选项(Ant Design监听mousedown/pointerdown) + console.log(`OfferPie: [Hotjob-AntSelect] 找到选项,点击: tag=${targetItem.tagName} | class="${targetItem.className}"`) + simulateClick(targetItem) + await delay("mid") + + console.log(`OfferPie: ✅ [Hotjob-AntSelect] 已选择 "${fillValue}"`) + return true +} + +/** + * Hotjob平台特殊处理:时间/日期字段填写 + * 原因:Hotjob的Ant DatePicker弹出层渲染在页面DOM最末尾(div.ant-calendar-picker-container), + * 通用的 analyzeCalendarPanel 全DOM搜索找不到面板(被input附近元素干扰) + * 流程:simulateClick展开面板 → 全DOM搜ant-calendar-picker-container定位面板 → 调fillDatePicker(field, panelHint)走集成式逻辑 + * @param field - 匹配到的表单字段 + * @returns 是否成功填写 + */ +async function hotjobFillDateField(field: MatchedFormField): Promise { + const { labelText, inputElement, fillValue } = field + if (!inputElement || !fillValue) return false + + // 1. 如果fillValue只有年月(如2025-09),补成2025-09-01 + let adjustedValue = fillValue + if (/^\d{4}[.\-\/]\d{1,2}$/.test(fillValue)) { + adjustedValue = fillValue.replace(/[.\-\/]/, "-") + "-01" + console.log(`OfferPie: [Hotjob-Date] "${labelText}" 只有年月,补日为: "${adjustedValue}"`) + } + field.fillValue = adjustedValue + + // 2. simulateClick展开日期面板 + simulateClick(inputElement as HTMLElement) + await delay("mid") + + // 3. 全DOM搜索可见的 ant-calendar-picker-container,再往下找 ant-calendar-date-panel 作为精确面板定位 + const allContainers = document.querySelectorAll("div.ant-calendar-picker-container") + let panelHint: HTMLElement | null = null + for (const el of Array.from(allContainers)) { + const htmlEl = el as HTMLElement + if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0) { + // 优先找更精确的 ant-calendar-date-panel(包含头部年月按钮+日期格子) + const datePanel = htmlEl.querySelector("div.ant-calendar-date-panel") as HTMLElement | null + if (datePanel && datePanel.offsetHeight > 0) { + panelHint = datePanel + } else { + panelHint = htmlEl + } + break + } + } + + if (!panelHint) { + console.log(`OfferPie: ❌ [Hotjob-Date] "${labelText}" 未找到 ant-calendar-picker-container`) + return false + } + + console.log(`OfferPie: [Hotjob-Date] "${labelText}" 找到面板容器,调用 fillDatePicker`) + + // 4. 解析目标年月,通过Ant Calendar专属类名导航到目标年月 + const dateMatch = adjustedValue.match(/^(\d{4})[.\-\/](\d{1,2})/) + if (dateMatch) { + const targetYear = parseInt(dateMatch[1], 10) + const targetMonth = parseInt(dateMatch[2], 10) + + // 读取面板当前显示的年月(从 ant-calendar-year-select 和 ant-calendar-month-select 获取) + const readPanelYearMonth = (): { year: number; month: number } => { + const yearEl = panelHint!.querySelector("a.ant-calendar-year-select") + const monthEl = panelHint!.querySelector("a.ant-calendar-month-select") + const yearText = yearEl?.textContent?.trim() || "" + const monthText = monthEl?.textContent?.trim() || "" + const year = parseInt(yearText, 10) || 0 + const month = parseInt(monthText, 10) || 0 + return { year, month } + } + + let current = readPanelYearMonth() + console.log(`OfferPie: [Hotjob-Date] "${labelText}" 面板当前: ${current.year}年${current.month}月 → 目标: ${targetYear}年${targetMonth}月`) + + // 导航年份 + const yearDiff = targetYear - current.year + if (yearDiff !== 0) { + const yearBtnClass = yearDiff > 0 ? "a.ant-calendar-next-year-btn" : "a.ant-calendar-prev-year-btn" + const yearBtn = panelHint!.querySelector(yearBtnClass) as HTMLElement | null + if (yearBtn) { + for (let i = 0; i < Math.abs(yearDiff) && i < 100; i++) { + yearBtn.click() + await delay("low") + } + await delay("low") + current = readPanelYearMonth() + console.log(`OfferPie: [Hotjob-Date] 年份导航完成,当前: ${current.year}年${current.month}月`) + } + } + + // 导航月份 + const monthDiff = targetMonth - current.month + if (monthDiff !== 0) { + const monthBtnClass = monthDiff > 0 ? "a.ant-calendar-next-month-btn" : "a.ant-calendar-prev-month-btn" + const monthBtn = panelHint!.querySelector(monthBtnClass) as HTMLElement | null + if (monthBtn) { + for (let i = 0; i < Math.abs(monthDiff) && i < 12; i++) { + monthBtn.click() + await delay("low") + } + await delay("low") + current = readPanelYearMonth() + console.log(`OfferPie: [Hotjob-Date] 月份导航完成,当前: ${current.year}年${current.month}月`) + } + } + } + + // 5. 调用集成式选择器核心逻辑,传入panelHint让它在正确的容器内点击日期格子 + const ok = await fillDatePicker(field, panelHint) + return ok +} + /** Hotjob模式自动填写的参数 */ export interface AutoFillHotjobParams { /** 简历数据(接口获取的) */ @@ -315,6 +478,7 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis // 根据字段类型选择对应的填充方法(全部走已封装的统一入口) if (isTimePeriodField(f.key)) { + console.log(`OfferPie: [Hotjob-Debug] 进入 isTimePeriodField 分支: key="${f.key}" labelText="${f.labelText}"`) const startDateVal = getResumeFieldValue(currentResumeData, f.section, dataIdx, "startDate") const endDateVal = getResumeFieldValue(currentResumeData, f.section, dataIdx, "endDate") let ok = await fillTimePeriodField(f, startDateVal, endDateVal, usedInputs) @@ -325,17 +489,23 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis } if (ok) { result.success++ } else { result.failed++ } } else if (isTimeSingleField(f.key)) { + console.log(`OfferPie: [Hotjob-Debug] 进入 isTimeSingleField 分支: key="${f.key}" labelText="${f.labelText}" fillValue="${f.fillValue}"`) if (!f.fillValue) { phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: false, fillValue: "", section: f.section, segmentIndex: segIdx }); result.skipped++; continue } - let ok = await fillTimeSingleField(f, f.fillValue, usedInputs) - if (!ok) { - await detectPickerField(f, lang) - ok = await fillMatchedField(f) - } + let ok: boolean + // Hotjob平台:所有时间字段都走hotjobFillDateField(Ant DatePicker弹出层在DOM末尾) + console.log(`OfferPie: [Hotjob-Debug] 走 hotjobFillDateField`) + ok = await hotjobFillDateField(f) if (ok) { result.success++ } else { result.failed++ } } else if (!f.fillValue) { phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: false, fillValue: "", section: f.section, segmentIndex: segIdx }); result.skipped++; continue } else if (isSearchPickerField(f.key)) { - const ok = await fillSearchPickerField(f) + let ok: boolean + // Hotjob平台特殊字段:弹出层在DOM末尾,通用搜索逻辑定位不到,走全DOM搜li选项 + if (HOTJOB_ANT_SELECT_LABELS.includes(f.labelText)) { + ok = await hotjobFillAntSelectField(f.inputElement, f.fillValue) + } else { + ok = await fillSearchPickerField(f) + } if (ok) { result.success++ } else { result.failed++ } phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx }) await delay("mid") @@ -1190,7 +1360,7 @@ async function b2FillDateTimeField( labelText: string, fillValue: string ): Promise { - // 构造 MatchedFormField(和阶段A一样,isPicker=true,不设 pickerDropdownElement) + // Hotjob平台:所有时间/日期字段都走hotjobFillDateField(Ant DatePicker弹出层在DOM末尾,通用逻辑无法定位) const field: MatchedFormField = { key: "", section: "main", resumeField: "", sectionIndex: 0, labelText, @@ -1205,17 +1375,11 @@ async function b2FillDateTimeField( pickerDropdownElement: null, pickerDropdownSelector: "", fillValue, } - - // 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker - inputEl.focus() - ;(inputEl as HTMLElement).click() - await delay("mid") - - const ok = await fillDatePicker(field) + const ok = await hotjobFillDateField(field) if (ok) { - console.log(`OfferPie: ✅ [B2-时间字段] "${labelText}" = "${fillValue}" 填写成功`) + console.log(`OfferPie: ✅ [B2-时间字段] "${labelText}" = "${fillValue}" 填写成功 (Hotjob模式)`) } else { - console.log(`OfferPie: ❌ [B2-时间字段] "${labelText}" = "${fillValue}" 填写失败`) + console.log(`OfferPie: ❌ [B2-时间字段] "${labelText}" = "${fillValue}" 填写失败 (Hotjob模式)`) } return ok } diff --git a/src/lib/datePicker.ts b/src/lib/datePicker.ts index 81ea7a8..e33425c 100644 --- a/src/lib/datePicker.ts +++ b/src/lib/datePicker.ts @@ -186,9 +186,31 @@ function isDayGridArea(el: HTMLElement): boolean { * 2. 从日期格子向上找星期行(日一二三四五六) * 3. 从星期行继续向上到面板顶部,识别年月显示和可点击按钮 */ -function analyzeCalendarPanel(): CalendarPanelInfo | null { +function analyzeCalendarPanel(panelHint?: HTMLElement): CalendarPanelInfo | null { // ---- 步骤1:找到日期格子区域 ---- - // 从常见的日历容器选择器开始搜索 + let dayGridArea: HTMLElement | null = null + let panelRoot: HTMLElement | null = null + + // 如果调用方传入了面板定位提示(panelHint),优先在提示元素内搜索日期格子 + if (panelHint) { + const innerEls = panelHint.querySelectorAll("table, tbody, div") + for (const inner of Array.from(innerEls)) { + if (isVisible(inner) && isDayGridArea(inner as HTMLElement)) { + dayGridArea = inner as HTMLElement + panelRoot = panelHint + break + } + } + if (dayGridArea) { + console.log(`OfferPie: [datePicker] 通过 panelHint 找到日期格子区域: ${dayGridArea.tagName}.${(dayGridArea.className || "").toString().split(" ")[0]}`) + } else { + console.log(`OfferPie: [datePicker] panelHint 内未找到日期格子,fallback 到全 DOM 搜索`) + } + } + + // 从常见的日历容器选择器开始搜索(panelHint 未命中时执行) + if (!dayGridArea) { + const panelSelectors = [ '[class*="picker-panel"]', '[class*="date-panel"]', '[class*="calendar"]', '[class*="datepicker"]', '[class*="date-picker"]', @@ -197,9 +219,6 @@ function analyzeCalendarPanel(): CalendarPanelInfo | null { '[class*="dropdown"]', '[class*="overlay"]', ] - let dayGridArea: HTMLElement | null = null - let panelRoot: HTMLElement | null = null - // 先尝试从已知面板选择器中找 for (const sel of panelSelectors) { const panels = document.querySelectorAll(sel) @@ -247,6 +266,8 @@ function analyzeCalendarPanel(): CalendarPanelInfo | null { } } + } // end if (!dayGridArea) — panelHint 未命中时的全 DOM 搜索 + if (!dayGridArea || !panelRoot) { console.warn("OfferPie: [datePicker] 未找到日期格子区域") return null @@ -1264,7 +1285,7 @@ function parseDateValue(fillValue: string): { year: number; month: number; day: * * 【注意】按钮探测逻辑是通用适配的核心,不要简化或删除 */ -export async function fillDatePicker(field: MatchedFormField): Promise { +export async function fillDatePicker(field: MatchedFormField, panelHint?: HTMLElement): Promise { const { labelText, inputElement, fillValue } = field if (!inputElement) return false @@ -1289,6 +1310,10 @@ export async function fillDatePicker(field: MatchedFormField): Promise if (location.hostname.includes("mokahr.com") && (inputElement as HTMLInputElement).readOnly) { console.log(`OfferPie: [datePicker] Moka适配: 跳过 forceSetValue(readonly input),等待弹出层渲染`) await delay("high") + // 【Hotjob适配】Hotjob 的 Ant DatePicker input 为 readonly,写值会触发面板关闭,跳过 + } else if (location.hostname.includes("hotjob.cn") && (inputElement as HTMLInputElement).readOnly) { + console.log(`OfferPie: [datePicker] Hotjob适配: 跳过 forceSetValue(readonly input),等待弹出层渲染`) + await delay("mid") } else { forceSetValue(inputElement, fillValue) await delay("high") @@ -1302,7 +1327,7 @@ export async function fillDatePicker(field: MatchedFormField): Promise } // ---- 步骤3:分析日历面板结构 ---- - const panel = analyzeCalendarPanel() + const panel = analyzeCalendarPanel(panelHint) if (!panel) { console.warn("OfferPie: [datePicker] 无法分析日历面板,尝试直接点击日期格子") // 兜底:尝试旧逻辑直接找日期格子 diff --git a/src/lib/formStyle.ts b/src/lib/formStyle.ts index 504f013..6f4aa98 100644 --- a/src/lib/formStyle.ts +++ b/src/lib/formStyle.ts @@ -9,8 +9,8 @@ export type HighlightColor = "green" |"greenTwo" | "red" | "yellow" /** 颜色映射表(淡色系,不影响文字可读性) */ const COLOR_MAP: Record = { - green: "#b7ffc5", // 淡绿色 - greenTwo: "#b7ffc6", // 淡绿色2 + green: "#b7ffc64d", // 淡绿色 + greenTwo: "#b7ffc54d", // 淡绿色2 red: "#ffb7b7", // 淡红色 yellow: "transparent", // 透明色 } diff --git a/src/lib/pickerFill.ts b/src/lib/pickerFill.ts index 5b85665..47eb653 100644 --- a/src/lib/pickerFill.ts +++ b/src/lib/pickerFill.ts @@ -4,6 +4,7 @@ */ import type { MatchedFormField } from "./types" +import { simulateClick } from "~utils/domEvent" // ============ 模糊匹配 ============ @@ -62,7 +63,10 @@ export function clickBestOptionInDropdown(dropdownEl: HTMLElement, fillValue: st let bestMatch: { el: HTMLElement; text: string; score: number } | null = null for (const { el, text } of candidates) { if (text.toLowerCase() === fillValue.toLowerCase()) { - el.click() + // 【注意】必须用 simulateClick 触发完整事件链(pointerdown→mousedown→pointerup→mouseup→click) + // 原生 .click() 只派发 click 事件,Ant Design 等组件库监听的是 mousedown/pointerdown,用 .click() 选项不会生效 + console.log(`OfferPie: [clickBestOption] 即将点击元素: tag=${el.tagName} | class="${el.className}" | role="${el.getAttribute("role")}"`) + simulateClick(el) console.log(`OfferPie: ✅ [弹出层] 点击选项 "${text}" (精确匹配)`) return true } @@ -70,7 +74,8 @@ export function clickBestOptionInDropdown(dropdownEl: HTMLElement, fillValue: st if (score > 0.2 && (!bestMatch || score > bestMatch.score)) bestMatch = { el, text, score } } if (bestMatch) { - bestMatch.el.click() + // 【注意】同上,必须用 simulateClick 触发完整事件链,否则 Ant Design 等组件不响应 + simulateClick(bestMatch.el) console.log(`OfferPie: ✅ [弹出层] 点击选项 "${bestMatch.text}" (模糊匹配 score=${bestMatch.score.toFixed(2)})`) return true }