重复填写添加经历段数校正、文档规范hook、调整填写速度
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
import { EXPERIENCE_SECTION_CONFIGS, JOB_FORM_LABELS } from "./constants"
|
||||
import { delay } from "~utils/delay"
|
||||
import { snapshotElementsInRange, diffSnapshots, findTopLevelNewElements } from "./dom"
|
||||
import { findNearestInput } from "./formMatcher"
|
||||
import type { ExperienceSection, ExperienceSectionConfig, ResumeData } from "./types"
|
||||
|
||||
/** 大标题排除关键词:包含这些文字的标签不作为大标题(它们是子标题/说明文字) */
|
||||
@@ -534,6 +535,82 @@ function buildLocator(container: Element, parent: Element, nthIndex: number, tit
|
||||
return { ancestor1, ancestor2, pathFromAncestor1, nthIndex, sectionTitleText: titleText }
|
||||
}
|
||||
|
||||
/**
|
||||
* 【段数校正】通过核心字段标签的重复出现次数,统计指定经历区块内已有的经历段数
|
||||
*
|
||||
* 原理:
|
||||
* 每段经历必然包含一个核心字段(如教育经历的"学校名称"、工作经历的"公司名称")。
|
||||
* 在大标题范围内统计核心字段标签出现的次数(且标签后面紧跟有效 input),
|
||||
* 即可得知当前已展开了几段经历。
|
||||
*
|
||||
* 用途:
|
||||
* 在 expandExperienceSections 进入添加循环前,用此方法校正 expandedCount,
|
||||
* 避免重复填写场景下因 expandedCount 初始化为1而多添加空白段。
|
||||
*
|
||||
* 安全退化:
|
||||
* 如果核心字段标签在范围内一个都没匹配到,返回 0,
|
||||
* 调用方用 Math.max 保留原值,行为和修复前一致。
|
||||
*
|
||||
* @param titleEl - 经历大标题元素(范围起始)
|
||||
* @param nextTitleEl - 下一个大标题元素(范围结束,null 表示到页面底部)
|
||||
* @param config - 当前经历类型的配置(含 coreFieldKey)
|
||||
* @param lang - 页面语言
|
||||
* @returns 检测到的已有段数(0 表示未检测到核心字段)
|
||||
*/
|
||||
function countExistingSegments(
|
||||
titleEl: Element,
|
||||
nextTitleEl: Element | null,
|
||||
config: ExperienceSectionConfig,
|
||||
lang: "zh" | "en"
|
||||
): number {
|
||||
// 从 JOB_FORM_LABELS 中找到 coreFieldKey 对应的标签配置
|
||||
const coreLabel = JOB_FORM_LABELS.find((item) => item.key === config.coreFieldKey)
|
||||
if (!coreLabel) return 0
|
||||
|
||||
const keywords = lang === "zh" ? coreLabel.zh : coreLabel.en
|
||||
// 按长度倒序,优先匹配更精确的标签(如"学校名称"优先于"学校")
|
||||
const sortedKeywords = [...keywords].sort((a, b) => b.length - a.length)
|
||||
|
||||
// 在范围内遍历所有候选标签元素,统计核心字段标签出现次数
|
||||
const candidateSelector = "label, span, div, td, th, p, legend, dt"
|
||||
const allCandidates = document.body.querySelectorAll(candidateSelector)
|
||||
let count = 0
|
||||
|
||||
for (const el of Array.from(allCandidates)) {
|
||||
// 范围限定:必须在 titleEl 之后
|
||||
if (!(titleEl.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) continue
|
||||
// 范围限定:必须在 nextTitleEl 之前
|
||||
if (nextTitleEl && !(nextTitleEl.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)) continue
|
||||
|
||||
// 只用元素自身的直接文本匹配(避免匹配到包含多层子元素的大容器)
|
||||
const directText = Array.from(el.childNodes)
|
||||
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.textContent?.trim())
|
||||
.filter(Boolean)
|
||||
.join("")
|
||||
|
||||
if (!directText) continue
|
||||
|
||||
// 匹配核心字段关键词(文字包含关键词且长度接近,防止误匹配长文本)
|
||||
const matched = sortedKeywords.find(
|
||||
(keyword) => directText.includes(keyword) && directText.length < keyword.length + 20
|
||||
)
|
||||
if (!matched) continue
|
||||
|
||||
// 【关键验证】标签后面必须紧跟有效 input,否则不计数(过滤说明性文字)
|
||||
const inputEl = findNearestInput(el)
|
||||
if (!inputEl) continue
|
||||
|
||||
// 验证找到的 input 也在范围内(防止跨区块匹配)
|
||||
if (!(titleEl.compareDocumentPosition(inputEl) & Node.DOCUMENT_POSITION_FOLLOWING)) continue
|
||||
if (nextTitleEl && !(nextTitleEl.compareDocumentPosition(inputEl) & Node.DOCUMENT_POSITION_PRECEDING)) continue
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测默认第1段经历的边界
|
||||
* 在大标题到下一个大标题之间,找到第一个 input,然后向上找段落容器
|
||||
@@ -1059,6 +1136,19 @@ export async function expandExperienceSections(
|
||||
for (const result of locateResults) {
|
||||
if (!result.titleElement) continue
|
||||
|
||||
// 【段数校正】通过核心字段标签重复次数校正 expandedCount,防止重复填写时多添加空白段
|
||||
const config = EXPERIENCE_SECTION_CONFIGS.find((c) => c.section === result.section)
|
||||
if (config) {
|
||||
const allPageTitles = getPageTitles(locateResults, refSignature, firstLocated.titleElement)
|
||||
const idx = allPageTitles.findIndex((pt) => pt.element === result.titleElement)
|
||||
const nextTitleEl = idx >= 0 && idx < allPageTitles.length - 1 ? allPageTitles[idx + 1].element : null
|
||||
const detectedCount = countExistingSegments(result.titleElement, nextTitleEl, config, lang)
|
||||
if (detectedCount > result.expandedCount) {
|
||||
console.log(` [${result.section}] 段数校正: expandedCount ${result.expandedCount} → ${detectedCount}(通过核心字段标签计数)`)
|
||||
result.expandedCount = detectedCount
|
||||
}
|
||||
}
|
||||
|
||||
const resumeCount = (resumeData[result.section] as unknown[])?.length || 0
|
||||
if (resumeCount <= result.expandedCount) {
|
||||
console.log(` [${result.section}] 简历${resumeCount}段 ≤ 页面${result.expandedCount}段,无需添加`)
|
||||
@@ -1068,7 +1158,6 @@ export async function expandExperienceSections(
|
||||
const needAdd = resumeCount - result.expandedCount
|
||||
console.log(` [${result.section}] 简历${resumeCount}段 > 页面${result.expandedCount}段,需添加 ${needAdd} 段`)
|
||||
|
||||
const config = EXPERIENCE_SECTION_CONFIGS.find((c) => c.section === result.section)
|
||||
if (!config) continue
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,7 +61,7 @@ const INPUT_SEL = "input:not([type='hidden']):not([type='submit']):not([type='bu
|
||||
* 在标签元素附近查找最近的、属于同一表单项的 input/textarea 输入框
|
||||
* 策略:1.表单项容器内查找 → 2.逐级向上查找 → 3.兜底查兄弟节点
|
||||
*/
|
||||
function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextAreaElement | null {
|
||||
export function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextAreaElement | null {
|
||||
// 策略1:找标签所在的最近表单项容器
|
||||
for (const sel of FORM_ITEM_SELECTORS) {
|
||||
const container = labelEl.closest(sel)
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ export type DelayLevel = "low" | "mid" | "high" | "max"
|
||||
|
||||
/** 各等级对应的毫秒数(统一在此处调整) */
|
||||
const DELAY_MS: Record<DelayLevel, number> = {
|
||||
low: 50, // <100ms 场景:微等待,点击后极短暂停
|
||||
mid: 200, // 100~300ms 场景:等待 DOM 更新、弹出层渲染
|
||||
low: 10, // <100ms 场景:微等待,点击后极短暂停
|
||||
mid: 50, // 100~300ms 场景:等待 DOM 更新、弹出层渲染
|
||||
high: 500, // >300ms 场景:等待接口返回、搜索结果、动画完成
|
||||
max: 2000, // >2000ms 场景:特殊超长延时(谨慎使用)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user