经历识别添加
This commit is contained in:
@@ -12,6 +12,7 @@ import { detectPickerField } from "~lib/pickerDetector"
|
|||||||
import { detectAndUploadResume } from "~lib/resumeUpload"
|
import { detectAndUploadResume } from "~lib/resumeUpload"
|
||||||
import { getMockResumeData } from "~lib/constants"
|
import { getMockResumeData } from "~lib/constants"
|
||||||
import { getResumeFieldValue } from "~lib/resumeDataHelper"
|
import { getResumeFieldValue } from "~lib/resumeDataHelper"
|
||||||
|
import { locateExperienceSections, expandExperienceSections } from "~lib/experienceSection"
|
||||||
import type { MatchedFormField, ResumeData } from "~lib/types"
|
import type { MatchedFormField, ResumeData } from "~lib/types"
|
||||||
import "./SidebarPanel.scss"
|
import "./SidebarPanel.scss"
|
||||||
|
|
||||||
@@ -63,18 +64,17 @@ export function SidebarPanel({ onClose }: SidebarPanelProps) {
|
|||||||
setResumeData(currentResumeData)
|
setResumeData(currentResumeData)
|
||||||
console.log(`===== OfferPie: 已加载简历数据,教育${currentResumeData.education.length}段 工作${currentResumeData.work.length}段 实习${currentResumeData.internship.length}段 项目${currentResumeData.project.length}段 竞赛${currentResumeData.competition.length}段 =====`)
|
console.log(`===== OfferPie: 已加载简历数据,教育${currentResumeData.education.length}段 工作${currentResumeData.work.length}段 实习${currentResumeData.internship.length}段 项目${currentResumeData.project.length}段 竞赛${currentResumeData.competition.length}段 =====`)
|
||||||
|
|
||||||
// 4.1 检测并上传简历文件
|
// // 4.1 检测并上传简历文件
|
||||||
const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E4%BA%8E%E5%A4%A7%E6%98%A5.pdf"
|
// const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E4%BA%8E%E5%A4%A7%E6%98%A5.pdf"
|
||||||
const uploaded = await detectAndUploadResume(resumeUrl)
|
// const uploaded = await detectAndUploadResume(resumeUrl)
|
||||||
console.log(`===== OfferPie: 简历上传 ${uploaded ? "成功" : "跳过(未找到上传按钮或失败)"} =====`)
|
// console.log(`===== OfferPie: 简历上传 ${uploaded ? "成功" : "跳过(未找到上传按钮或失败)"} =====`)
|
||||||
if (uploaded) await delay(1000) // 等待网站解析简历
|
// if (uploaded) await delay(1000) // 等待网站解析简历
|
||||||
|
|
||||||
// 4.5 查找教育背景(教育经历)、实习经历、工作经历、项目经历、竞赛经历位置和添加按钮,根据要填的简历数据添加相应的经历条数
|
// 4.5 定位经历区块并统计已展开段数
|
||||||
|
const sectionResults = locateExperienceSections(document.body, lang)
|
||||||
|
// 4.6 对比简历数据段数,点击添加按钮补足不够的段数
|
||||||
|
const expandedResults = await expandExperienceSections(sectionResults, currentResumeData, lang)
|
||||||
// TODO: 后续实现步骤:
|
// TODO: 后续实现步骤:
|
||||||
// 1. 遍历 EXPERIENCE_SECTION_CONFIGS,在 DOM 中查找每种经历的区块标题元素
|
|
||||||
// 2. 对比简历数据中该经历的段数 vs 页面已展开的段数
|
|
||||||
// 3. 如果简历数据段数 > 页面已展开段数,查找并点击"添加"按钮补足差额
|
|
||||||
// 4. 每次点击添加按钮后等待 DOM 更新,重新计数确认
|
|
||||||
// 5. 对5种经历的标签关系数据做特殊标记(后续完善)
|
// 5. 对5种经历的标签关系数据做特殊标记(后续完善)
|
||||||
// 6. 所有经历段数添加完毕后,再进入统一的字段匹配和填写流程
|
// 6. 所有经历段数添加完毕后,再进入统一的字段匹配和填写流程
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ export function SidebarPanel({ onClose }: SidebarPanelProps) {
|
|||||||
<div className="op-container">
|
<div className="op-container">
|
||||||
{/* 顶部操作栏:反馈、设置、关闭按钮 */}
|
{/* 顶部操作栏:反馈、设置、关闭按钮 */}
|
||||||
<div className="op-header">
|
<div className="op-header">
|
||||||
<span className="op-header-link">反馈3</span>
|
<span className="op-header-link">反馈5</span>
|
||||||
<span className="op-header-link">设置</span>
|
<span className="op-header-link">设置</span>
|
||||||
{/* 关闭按钮:点击后隐藏侧边栏 */}
|
{/* 关闭按钮:点击后隐藏侧边栏 */}
|
||||||
<button className="op-close-btn" onClick={onClose}>
|
<button className="op-close-btn" onClick={onClose}>
|
||||||
|
|||||||
+44
-5
@@ -224,7 +224,7 @@ export const EXPERIENCE_SECTION_CONFIGS: ExperienceSectionConfig[] = [
|
|||||||
zh: ["教育背景", "教育经历", "学历信息", "教育信息"],
|
zh: ["教育背景", "教育经历", "学历信息", "教育信息"],
|
||||||
en: ["Education", "Education Background", "Academic Background", "Education History"],
|
en: ["Education", "Education Background", "Academic Background", "Education History"],
|
||||||
coreFieldKey: "school",
|
coreFieldKey: "school",
|
||||||
addButtonZh: ["添加", "新增", "添加教育", "添加教育经历", "新增教育经历", "+ 添加"],
|
addButtonZh: ["添加", "新增", "添加教育", "添加教育经历", "新增教育经历", "+ 添加", "+ 新增"],
|
||||||
addButtonEn: ["Add", "Add Education", "Add More", "+ Add"],
|
addButtonEn: ["Add", "Add Education", "Add More", "+ Add"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -232,7 +232,7 @@ export const EXPERIENCE_SECTION_CONFIGS: ExperienceSectionConfig[] = [
|
|||||||
zh: ["工作经历", "工作经验", "工作信息"],
|
zh: ["工作经历", "工作经验", "工作信息"],
|
||||||
en: ["Work Experience", "Employment History", "Work History", "Professional Experience"],
|
en: ["Work Experience", "Employment History", "Work History", "Professional Experience"],
|
||||||
coreFieldKey: "workCompany",
|
coreFieldKey: "workCompany",
|
||||||
addButtonZh: ["添加", "新增", "添加工作", "添加工作经历", "新增工作经历", "+ 添加"],
|
addButtonZh: ["添加", "新增", "添加工作", "添加工作经历", "新增工作经历", "+ 添加", "+ 新增"],
|
||||||
addButtonEn: ["Add", "Add Work", "Add Experience", "Add More", "+ Add"],
|
addButtonEn: ["Add", "Add Work", "Add Experience", "Add More", "+ Add"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -240,7 +240,7 @@ export const EXPERIENCE_SECTION_CONFIGS: ExperienceSectionConfig[] = [
|
|||||||
zh: ["实习经历", "实习经验", "实习信息"],
|
zh: ["实习经历", "实习经验", "实习信息"],
|
||||||
en: ["Internship", "Internship Experience", "Intern Experience"],
|
en: ["Internship", "Internship Experience", "Intern Experience"],
|
||||||
coreFieldKey: "internCompany",
|
coreFieldKey: "internCompany",
|
||||||
addButtonZh: ["添加", "新增", "添加实习", "添加实习经历", "新增实习经历", "+ 添加"],
|
addButtonZh: ["添加", "新增", "添加实习", "添加实习经历", "新增实习经历", "+ 添加", "+ 新增"],
|
||||||
addButtonEn: ["Add", "Add Internship", "Add More", "+ Add"],
|
addButtonEn: ["Add", "Add Internship", "Add More", "+ Add"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -248,7 +248,7 @@ export const EXPERIENCE_SECTION_CONFIGS: ExperienceSectionConfig[] = [
|
|||||||
zh: ["项目经历", "项目经验", "项目信息"],
|
zh: ["项目经历", "项目经验", "项目信息"],
|
||||||
en: ["Project", "Project Experience", "Projects"],
|
en: ["Project", "Project Experience", "Projects"],
|
||||||
coreFieldKey: "projectName",
|
coreFieldKey: "projectName",
|
||||||
addButtonZh: ["添加", "新增", "添加项目", "添加项目经历", "新增项目经历", "+ 添加"],
|
addButtonZh: ["添加", "新增", "添加项目", "添加项目经历", "新增项目经历", "+ 添加", "+ 新增"],
|
||||||
addButtonEn: ["Add", "Add Project", "Add More", "+ Add"],
|
addButtonEn: ["Add", "Add Project", "Add More", "+ Add"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -256,7 +256,7 @@ export const EXPERIENCE_SECTION_CONFIGS: ExperienceSectionConfig[] = [
|
|||||||
zh: ["竞赛经历", "获奖经历", "竞赛信息", "获奖情况"],
|
zh: ["竞赛经历", "获奖经历", "竞赛信息", "获奖情况"],
|
||||||
en: ["Competition", "Awards", "Competitions & Awards", "Contest Experience"],
|
en: ["Competition", "Awards", "Competitions & Awards", "Contest Experience"],
|
||||||
coreFieldKey: "competitionName",
|
coreFieldKey: "competitionName",
|
||||||
addButtonZh: ["添加", "新增", "添加竞赛", "添加竞赛经历", "新增竞赛经历", "+ 添加"],
|
addButtonZh: ["添加", "新增", "添加竞赛", "添加竞赛经历", "新增竞赛经历", "+ 添加", "+ 新增"],
|
||||||
addButtonEn: ["Add", "Add Competition", "Add Award", "Add More", "+ Add"],
|
addButtonEn: ["Add", "Add Competition", "Add Award", "Add More", "+ Add"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -310,6 +310,21 @@ export function getMockResumeData(): ResumeData {
|
|||||||
{ type: "text", content: "熟练使用Office办公软件(Word、Excel、PPT)进行数据整理、报告撰写与汇报展示,注重细节与准确性" },
|
{ type: "text", content: "熟练使用Office办公软件(Word、Excel、PPT)进行数据整理、报告撰写与汇报展示,注重细节与准确性" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "AntTOPmj",
|
||||||
|
school: "华南师范大学2",
|
||||||
|
major: "人工智能",
|
||||||
|
degree: "本科",
|
||||||
|
studyType: "统招",
|
||||||
|
startDate: "2021.09",
|
||||||
|
endDate: "2025.07",
|
||||||
|
description: [
|
||||||
|
{ type: "text", content: "大一:高等数学、Python程序设计、面向对象程序设计、计算机体系结构、线性代数、离散数学等,培养严谨的逻辑思维能力" },
|
||||||
|
{ type: "text", content: "大二:Java、软件工程导论、概率论与数理统计、数据结构与算法、Web开发、数据库原理、操作系统等,具备数据管理与分析基础" },
|
||||||
|
{ type: "text", content: "大三:人工智能基础、计算机网络、计算机语言等,掌握数据挖掘与模式识别方法" },
|
||||||
|
{ type: "text", content: "熟练使用Office办公软件(Word、Excel、PPT)进行数据整理、报告撰写与汇报展示,注重细节与准确性" },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
work: [],
|
work: [],
|
||||||
internship: [
|
internship: [
|
||||||
@@ -337,6 +352,30 @@ export function getMockResumeData(): ResumeData {
|
|||||||
{ type: "text", content: "熟练掌握Office办公软件及数据库工具,具备文献检索与信息整理经验,工作态度认真负责。" },
|
{ type: "text", content: "熟练掌握Office办公软件及数据库工具,具备文献检索与信息整理经验,工作态度认真负责。" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "EoNkjAF2",
|
||||||
|
companyName: "深圳乐动机器人科技有限公司2",
|
||||||
|
position: "机器人感知算法工程师",
|
||||||
|
startDate: "2024.05",
|
||||||
|
endDate: "2024.08",
|
||||||
|
description: [
|
||||||
|
{ type: "text", content: "负责数据收集、分析、模型训练与测试、算法部署等全流程工作,注重数据准确性与细节管理;" },
|
||||||
|
{ type: "text", content: "参与割草机算法研发工作,包括分类、检测、分割、跟踪等模块,具备快速学习新技术领域的能力;" },
|
||||||
|
{ type: "text", content: "撰写技术文档与测试报告,进行跨部门沟通协作,确保项目按时交付并符合质量标准。" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cX5EGRDA",
|
||||||
|
companyName: "江西卓云深圳研发中心2",
|
||||||
|
position: "图像算法工程师",
|
||||||
|
startDate: "2023.12",
|
||||||
|
endDate: "2024.02",
|
||||||
|
description: [
|
||||||
|
{ type: "text", content: "运用Python进行数据处理与分析,开发部署应用程序,具备较强的逻辑分析和数据整理能力;" },
|
||||||
|
{ type: "text", content: "独立完成项目全生命周期管理,包括数据收集、标注、模型训练、部署及性能调优,能应对重复性工作并保证准确性;" },
|
||||||
|
{ type: "text", content: "熟练掌握Office办公软件及数据库工具,具备文献检索与信息整理经验,工作态度认真负责。" },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
project: [
|
project: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,652 @@
|
|||||||
|
/**
|
||||||
|
* 经历区块识别、段数统计与添加按钮点击模块
|
||||||
|
*
|
||||||
|
* 整体流程:
|
||||||
|
* 1. 定位页面中5大经历区块的大标题
|
||||||
|
* 2. 根据 TitleSignature 找出页面所有同结构大标题,确定管辖范围边界
|
||||||
|
* 3. 在管辖范围内统计已展开段数(核心字段标签+输入框组合数量)
|
||||||
|
* 4. 对比简历数据段数,不够则查找并点击"添加"按钮补足
|
||||||
|
* 5. 每次点击后重新确认段数,记录新增段的 DOM 结构信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EXPERIENCE_SECTION_CONFIGS, JOB_FORM_LABELS } from "./constants"
|
||||||
|
import { delay } from "./autofill"
|
||||||
|
import type { ExperienceSection, ExperienceSectionConfig, ResumeData } from "./types"
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// 一、类型定义
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
/** 大标题的 HTML 标签 + CSS 类名特征 */
|
||||||
|
export interface TitleSignature {
|
||||||
|
/** 第一层有类名的祖先:标签名 + 完整类名 */
|
||||||
|
layer1: { tag: string; className: string } | null
|
||||||
|
/** 第二层有类名的祖先:标签名 + 完整类名 */
|
||||||
|
layer2: { tag: string; className: string } | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页面中所有大标题的信息(包括非经历标题) */
|
||||||
|
interface PageTitle {
|
||||||
|
element: Element
|
||||||
|
text: string
|
||||||
|
signature: TitleSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 经历区块定位结果 */
|
||||||
|
export interface ExperienceSectionLocateResult {
|
||||||
|
section: ExperienceSection
|
||||||
|
titleElement: Element | null
|
||||||
|
titleText: string
|
||||||
|
titleSignature: TitleSignature
|
||||||
|
/** 页面已展开的段数 */
|
||||||
|
expandedCount: number
|
||||||
|
/** 每段经历的 DOM 范围信息(用于后续填写时定位) */
|
||||||
|
segmentRanges: SegmentRange[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单段经历的 DOM 范围信息 */
|
||||||
|
export interface SegmentRange {
|
||||||
|
/** 该段经历的起始标志元素(核心字段标签元素) */
|
||||||
|
startElement: Element
|
||||||
|
/** 该段经历的起始标志文字 */
|
||||||
|
startText: string
|
||||||
|
/** 该段经历是否为新增的(通过点击添加按钮产生) */
|
||||||
|
isNewlyAdded: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// 二、大标题定位相关
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 DOM 中查找经历区块的大标题元素
|
||||||
|
* 用 TreeWalker 深度优先遍历,找到直接包含标题文字的最内层元素
|
||||||
|
*/
|
||||||
|
function findSectionTitle(
|
||||||
|
rootEl: Element,
|
||||||
|
lang: "zh" | "en",
|
||||||
|
config: ExperienceSectionConfig
|
||||||
|
): { titleElement: Element; titleText: string } | null {
|
||||||
|
const keywords = lang === "zh" ? config.zh : config.en
|
||||||
|
const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_ELEMENT)
|
||||||
|
let node: Node | null = walker.nextNode()
|
||||||
|
|
||||||
|
while (node) {
|
||||||
|
const el = node as Element
|
||||||
|
const directText = Array.from(el.childNodes)
|
||||||
|
.filter((child) => child.nodeType === Node.TEXT_NODE)
|
||||||
|
.map((child) => child.textContent?.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("")
|
||||||
|
|
||||||
|
if (directText) {
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (directText.includes(keyword) && directText.length < keyword.length + 10) {
|
||||||
|
return { titleElement: el, titleText: directText }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = walker.nextNode()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取大标题元素的 TitleSignature
|
||||||
|
* 从标题元素的父级开始向上查找两层有 CSS 类名的祖先
|
||||||
|
*/
|
||||||
|
function extractTitleSignature(titleEl: Element): TitleSignature {
|
||||||
|
const signature: TitleSignature = { layer1: null, layer2: null }
|
||||||
|
let layerFound = 0
|
||||||
|
let current: Element | null = titleEl.parentElement
|
||||||
|
|
||||||
|
while (current && layerFound < 2) {
|
||||||
|
const cls = current.className && typeof current.className === "string" ? current.className.trim() : ""
|
||||||
|
if (cls) {
|
||||||
|
if (layerFound === 0) {
|
||||||
|
signature.layer1 = { tag: current.tagName.toLowerCase(), className: cls }
|
||||||
|
} else {
|
||||||
|
signature.layer2 = { tag: current.tagName.toLowerCase(), className: cls }
|
||||||
|
}
|
||||||
|
layerFound++
|
||||||
|
}
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
return signature
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据参考 TitleSignature,在页面中找出所有具有相同 layer2 类名的大标题
|
||||||
|
* 用于确定所有大标题的位置(包括非经历标题如"更新说明"、"授权文本"等)
|
||||||
|
*/
|
||||||
|
function findAllTitlesWithSameSignature(
|
||||||
|
rootEl: Element,
|
||||||
|
refSignature: TitleSignature
|
||||||
|
): PageTitle[] {
|
||||||
|
const results: PageTitle[] = []
|
||||||
|
if (!refSignature.layer2) return results
|
||||||
|
|
||||||
|
const layer2Classes = refSignature.layer2.className.split(/\s+/)
|
||||||
|
let containers: Element[]
|
||||||
|
try {
|
||||||
|
const selector = `.${CSS.escape(layer2Classes[0])}`
|
||||||
|
containers = Array.from(rootEl.querySelectorAll(selector))
|
||||||
|
} catch {
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const container of containers) {
|
||||||
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT)
|
||||||
|
let node: Node | null = walker.nextNode()
|
||||||
|
let foundTitle: PageTitle | null = null
|
||||||
|
|
||||||
|
while (node) {
|
||||||
|
const el = node as Element
|
||||||
|
const directText = Array.from(el.childNodes)
|
||||||
|
.filter((child) => child.nodeType === Node.TEXT_NODE)
|
||||||
|
.map((child) => child.textContent?.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("")
|
||||||
|
|
||||||
|
if (directText && directText.length > 0 && directText.length < 20 && el.children.length === 0) {
|
||||||
|
const sig = extractTitleSignature(el)
|
||||||
|
if (sig.layer2 && sig.layer2.className === refSignature.layer2!.className) {
|
||||||
|
foundTitle = { element: el, text: directText, signature: sig }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = walker.nextNode()
|
||||||
|
}
|
||||||
|
if (foundTitle) results.push(foundTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 DOM 顺序排列
|
||||||
|
results.sort((a, b) => {
|
||||||
|
const pos = a.element.compareDocumentPosition(b.element)
|
||||||
|
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1
|
||||||
|
if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// 三、段数统计相关
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
/** input 选择器 */
|
||||||
|
const INPUT_SEL = "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio']):not([type='file']):not([disabled]), textarea:not([disabled]), [class*='select'], [class*='picker']"
|
||||||
|
|
||||||
|
/** 每种经历类型的通用关联标签关键词(fallback 用) */
|
||||||
|
const SECTION_FALLBACK_KEYWORDS: Record<ExperienceSection, { zh: string[]; en: string[] }[]> = {
|
||||||
|
education: [
|
||||||
|
{ zh: ["学校", "学校名称", "毕业院校", "院校", "毕业学校"], en: ["School", "University", "College", "Institution"] },
|
||||||
|
],
|
||||||
|
work: [
|
||||||
|
{ zh: ["公司", "公司名称", "单位名称", "工作单位"], en: ["Company", "Company Name", "Employer"] },
|
||||||
|
],
|
||||||
|
internship: [
|
||||||
|
{ zh: ["公司", "公司名称", "单位名称", "实习公司", "实习单位"], en: ["Company", "Company Name", "Intern Company"] },
|
||||||
|
],
|
||||||
|
project: [
|
||||||
|
{ zh: ["项目名称", "项目名", "项目"], en: ["Project Name", "Project", "Project Title"] },
|
||||||
|
],
|
||||||
|
competition: [
|
||||||
|
{ zh: ["竞赛名称", "比赛名称", "竞赛"], en: ["Competition Name", "Contest Name", "Competition"] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取管辖范围内的所有候选标签元素
|
||||||
|
*/
|
||||||
|
function getRangeElements(titleEl: Element, nextTitleEl: Element | null): Element[] {
|
||||||
|
const allCandidates = document.body.querySelectorAll("label, span, div, td, th, p, dt, legend")
|
||||||
|
const rangeElements: Element[] = []
|
||||||
|
for (const el of Array.from(allCandidates)) {
|
||||||
|
if (!(titleEl.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) continue
|
||||||
|
if (nextTitleEl && !(nextTitleEl.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)) continue
|
||||||
|
rangeElements.push(el)
|
||||||
|
}
|
||||||
|
return rangeElements
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在给定元素列表中,数"关键词文本 + 输入框"组合的数量,并返回匹配到的标签元素
|
||||||
|
*/
|
||||||
|
function findLabelInputCombos(elements: Element[], keywords: string[]): Element[] {
|
||||||
|
const matched: Element[] = []
|
||||||
|
for (const el of elements) {
|
||||||
|
const directText = Array.from(el.childNodes)
|
||||||
|
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
||||||
|
.map((node) => node.textContent?.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("")
|
||||||
|
if (!directText) continue
|
||||||
|
|
||||||
|
let isMatch = false
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (directText.includes(keyword) && directText.length < keyword.length + 10) {
|
||||||
|
isMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isMatch) continue
|
||||||
|
if (hasNearbyInput(el)) matched.push(el)
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断标签元素附近是否有输入框
|
||||||
|
*/
|
||||||
|
function hasNearbyInput(labelEl: Element): boolean {
|
||||||
|
// 策略1:后续兄弟元素
|
||||||
|
let sibling = labelEl.nextElementSibling
|
||||||
|
let siblingCount = 0
|
||||||
|
while (sibling && siblingCount < 3) {
|
||||||
|
if (sibling.querySelector(INPUT_SEL) || sibling.matches(INPUT_SEL)) return true
|
||||||
|
sibling = sibling.nextElementSibling
|
||||||
|
siblingCount++
|
||||||
|
}
|
||||||
|
// 策略2:向上找父级容器
|
||||||
|
let parent: Element | null = labelEl.parentElement
|
||||||
|
for (let i = 0; i < 4 && parent; i++) {
|
||||||
|
const inputs = parent.querySelectorAll(INPUT_SEL)
|
||||||
|
if (inputs.length > 0) return true
|
||||||
|
parent = parent.parentElement
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计经历区块已展开段数
|
||||||
|
* 返回段数和匹配到的核心标签元素列表
|
||||||
|
*/
|
||||||
|
function countExpandedInRange(
|
||||||
|
titleEl: Element,
|
||||||
|
nextTitleEl: Element | null,
|
||||||
|
config: ExperienceSectionConfig,
|
||||||
|
lang: "zh" | "en"
|
||||||
|
): { count: number; matchedElements: Element[]; usedKeywords: string[] } {
|
||||||
|
const rangeElements = getRangeElements(titleEl, nextTitleEl)
|
||||||
|
|
||||||
|
// 第一轮:用该 section 自己的标签关键词
|
||||||
|
const sectionLabels = JOB_FORM_LABELS.filter((item) => item.section === config.section)
|
||||||
|
const coreLabel = sectionLabels.find((item) => item.key === config.coreFieldKey)
|
||||||
|
const orderedLabels = coreLabel
|
||||||
|
? [coreLabel, ...sectionLabels.filter((item) => item.key !== config.coreFieldKey)]
|
||||||
|
: sectionLabels
|
||||||
|
|
||||||
|
for (const labelItem of orderedLabels) {
|
||||||
|
const keywords = lang === "zh" ? labelItem.zh : labelItem.en
|
||||||
|
const matched = findLabelInputCombos(rangeElements, keywords)
|
||||||
|
if (matched.length > 0) {
|
||||||
|
return { count: matched.length, matchedElements: matched, usedKeywords: keywords }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二轮:用 fallback 关键词
|
||||||
|
const fallbacks = SECTION_FALLBACK_KEYWORDS[config.section]
|
||||||
|
if (fallbacks) {
|
||||||
|
for (const fb of fallbacks) {
|
||||||
|
const keywords = lang === "zh" ? fb.zh : fb.en
|
||||||
|
const matched = findLabelInputCombos(rangeElements, keywords)
|
||||||
|
if (matched.length > 0) {
|
||||||
|
return { count: matched.length, matchedElements: matched, usedKeywords: keywords }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count: 0, matchedElements: [], usedKeywords: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// 四、添加按钮查找与点击
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在经历大标题的管辖范围内查找最近的"添加"按钮
|
||||||
|
* 从大标题元素开始,在 DOM 从上到下的顺序中找第一个包含添加关键词的可点击元素
|
||||||
|
*/
|
||||||
|
function findAddButton(
|
||||||
|
titleEl: Element,
|
||||||
|
nextTitleEl: Element | null,
|
||||||
|
config: ExperienceSectionConfig,
|
||||||
|
lang: "zh" | "en"
|
||||||
|
): Element | null {
|
||||||
|
const addKeywords = lang === "zh" ? config.addButtonZh : config.addButtonEn
|
||||||
|
// 在管辖范围内查找包含添加关键词的元素
|
||||||
|
const allElements = document.body.querySelectorAll("button, a, span, div, [role='button']")
|
||||||
|
|
||||||
|
for (const el of Array.from(allElements)) {
|
||||||
|
// 必须在 titleEl 之后
|
||||||
|
if (!(titleEl.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) continue
|
||||||
|
// 必须在 nextTitleEl 之前
|
||||||
|
if (nextTitleEl && !(nextTitleEl.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)) continue
|
||||||
|
|
||||||
|
const text = el.textContent?.trim() || ""
|
||||||
|
// 匹配添加关键词
|
||||||
|
for (const keyword of addKeywords) {
|
||||||
|
if (text.includes(keyword) && text.length < keyword.length + 10) {
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击添加按钮(逐层点击,检测到段数增加立即停止)
|
||||||
|
*
|
||||||
|
* 策略:
|
||||||
|
* - 从添加文字的最内层元素开始,逐层向上点击
|
||||||
|
* - 每点一层后等待 DOM 更新,检测段数是否增加
|
||||||
|
* - 一旦某层点击后段数增加了,立即停止,不再继续往上冒泡
|
||||||
|
* - 如果5层内找到 button 标签,最多点到 button 层
|
||||||
|
* - 跳过包含输入框/富文本/radio 的层级(这些不可能是添加按钮)
|
||||||
|
*
|
||||||
|
* @param addBtnEl - 包含添加文字的元素
|
||||||
|
* @param titleEl - 当前经历大标题元素
|
||||||
|
* @param nextTitleEl - 下一个大标题元素(范围边界)
|
||||||
|
* @param config - 经历区块配置
|
||||||
|
* @param lang - 页面语言
|
||||||
|
* @returns 点击后是否成功增加了段数
|
||||||
|
*/
|
||||||
|
async function clickAddButton(
|
||||||
|
addBtnEl: Element,
|
||||||
|
titleEl: Element,
|
||||||
|
nextTitleEl: Element | null,
|
||||||
|
config: ExperienceSectionConfig,
|
||||||
|
lang: "zh" | "en"
|
||||||
|
): Promise<boolean> {
|
||||||
|
// 找到包含添加文字的最内层元素
|
||||||
|
let innerEl: Element = addBtnEl
|
||||||
|
while (innerEl.children.length === 1) {
|
||||||
|
const child = innerEl.children[0]
|
||||||
|
if (child.textContent?.trim() === addBtnEl.textContent?.trim()) {
|
||||||
|
innerEl = child
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从最内层向外收集要点击的层级(最多5层)
|
||||||
|
const clickTargets: HTMLElement[] = []
|
||||||
|
let current: Element | null = innerEl
|
||||||
|
|
||||||
|
for (let i = 0; i < 5 && current; i++) {
|
||||||
|
// 跳过包含输入框/富文本/radio 的层级
|
||||||
|
const hasFormEl = current.querySelector("input, textarea, [type='radio'], [contenteditable='true']")
|
||||||
|
if (!hasFormEl) {
|
||||||
|
clickTargets.push(current as HTMLElement)
|
||||||
|
}
|
||||||
|
// 如果当前层是 button,收集后停止向上
|
||||||
|
if (current.tagName.toLowerCase() === "button") break
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录点击前的段数
|
||||||
|
const beforeCount = countExpandedInRange(titleEl, nextTitleEl, config, lang).count
|
||||||
|
|
||||||
|
// 逐层点击,每层点击后检测段数是否增加
|
||||||
|
for (const target of clickTargets) {
|
||||||
|
target.click()
|
||||||
|
await delay(400) // 等待 DOM 更新
|
||||||
|
|
||||||
|
// 检测段数是否增加
|
||||||
|
const afterCount = countExpandedInRange(titleEl, nextTitleEl, config, lang).count
|
||||||
|
if (afterCount > beforeCount) {
|
||||||
|
// 段数增加了,停止冒泡
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有层都点了但段数没变化
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// 五、段数补足主流程
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位页面中所有经历区块并统计已展开段数(不含添加操作)
|
||||||
|
* 这是第一步:只做定位和统计,返回结果供后续使用
|
||||||
|
*/
|
||||||
|
export function locateExperienceSections(
|
||||||
|
rootEl: Element = document.body,
|
||||||
|
lang: "zh" | "en"
|
||||||
|
): ExperienceSectionLocateResult[] {
|
||||||
|
console.log("===== OfferPie: 开始定位经历区块 =====")
|
||||||
|
|
||||||
|
// 定位5种经历的大标题
|
||||||
|
const located: { config: ExperienceSectionConfig; titleElement: Element; titleText: string }[] = []
|
||||||
|
const notFound: ExperienceSectionConfig[] = []
|
||||||
|
|
||||||
|
for (const config of EXPERIENCE_SECTION_CONFIGS) {
|
||||||
|
const result = findSectionTitle(rootEl, lang, config)
|
||||||
|
if (result) {
|
||||||
|
located.push({ config, titleElement: result.titleElement, titleText: result.titleText })
|
||||||
|
} else {
|
||||||
|
notFound.push(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (located.length === 0) {
|
||||||
|
console.log(" ❌ 未找到任何经历区块大标题")
|
||||||
|
console.log("===== OfferPie: 经历区块定位完毕 =====")
|
||||||
|
return notFound.map((config) => ({
|
||||||
|
section: config.section, titleElement: null, titleText: "",
|
||||||
|
titleSignature: { layer1: null, layer2: null }, expandedCount: 0, segmentRanges: [],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用第一个找到的大标题的 TitleSignature 找出页面所有同结构大标题
|
||||||
|
const refSignature = extractTitleSignature(located[0].titleElement)
|
||||||
|
const allPageTitles = findAllTitlesWithSameSignature(rootEl, refSignature)
|
||||||
|
|
||||||
|
console.log(` --- 页面所有大标题(共 ${allPageTitles.length} 个,按 DOM 顺序)---`)
|
||||||
|
for (const pt of allPageTitles) {
|
||||||
|
console.log(` 标题: "${pt.text}" | TitleSignature: layer1=${pt.signature.layer1 ? `<${pt.signature.layer1.tag} class="${pt.signature.layer1.className}">` : "null"} layer2=${pt.signature.layer2 ? `<${pt.signature.layer2.tag} class="${pt.signature.layer2.className}">` : "null"}`)
|
||||||
|
}
|
||||||
|
console.log(` --- 大标题列表结束 ---`)
|
||||||
|
|
||||||
|
// 对每个经历大标题统计段数
|
||||||
|
const results: ExperienceSectionLocateResult[] = []
|
||||||
|
|
||||||
|
for (const { config, titleElement, titleText } of located) {
|
||||||
|
const titleSignature = extractTitleSignature(titleElement)
|
||||||
|
const idx = allPageTitles.findIndex((pt) => pt.element === titleElement)
|
||||||
|
const nextTitleEl = idx >= 0 && idx < allPageTitles.length - 1 ? allPageTitles[idx + 1].element : null
|
||||||
|
|
||||||
|
const { count, matchedElements, usedKeywords } = countExpandedInRange(titleElement, nextTitleEl, config, lang)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
` [${config.section}] ✅ 标题: "${titleText}"` +
|
||||||
|
` | 范围边界: "${allPageTitles[idx + 1]?.text || "页面底部"}"` +
|
||||||
|
` | 计数关键词: (${usedKeywords.join("/")})` +
|
||||||
|
` | 标签+输入框组合 ${count} 个`
|
||||||
|
)
|
||||||
|
console.log(` → 已展开段数: ${count}`)
|
||||||
|
|
||||||
|
// 记录每段经历的起始标志
|
||||||
|
const segmentRanges: SegmentRange[] = matchedElements.map((el) => ({
|
||||||
|
startElement: el,
|
||||||
|
startText: el.textContent?.trim() || "",
|
||||||
|
isNewlyAdded: false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
results.push({ section: config.section, titleElement, titleText, titleSignature, expandedCount: count, segmentRanges })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未找到的区块
|
||||||
|
for (const config of notFound) {
|
||||||
|
console.log(` [${config.section}] ❌ 未找到区块标题(关键词: ${(lang === "zh" ? config.zh : config.en).join("/")})`)
|
||||||
|
results.push({
|
||||||
|
section: config.section, titleElement: null, titleText: "",
|
||||||
|
titleSignature: { layer1: null, layer2: null }, expandedCount: 0, segmentRanges: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("===== OfferPie: 经历区块定位完毕 =====")
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补足经历段数:对比简历数据,点击添加按钮补足不够的段数
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 对每种经历,对比简历数据段数 vs 页面已展开段数
|
||||||
|
* 2. 如果简历段数 > 页面段数,查找并点击"添加"按钮
|
||||||
|
* 3. 每次点击后等待 DOM 更新,重新统计段数确认是否增加
|
||||||
|
* 4. 记录新增段的 DOM 信息
|
||||||
|
*
|
||||||
|
* @param locateResults - locateExperienceSections 的返回结果
|
||||||
|
* @param resumeData - 简历数据
|
||||||
|
* @param lang - 页面语言
|
||||||
|
* @returns 更新后的定位结果(含新增段信息)
|
||||||
|
*/
|
||||||
|
export async function expandExperienceSections(
|
||||||
|
locateResults: ExperienceSectionLocateResult[],
|
||||||
|
resumeData: ResumeData,
|
||||||
|
lang: "zh" | "en"
|
||||||
|
): Promise<ExperienceSectionLocateResult[]> {
|
||||||
|
console.log("===== OfferPie: 开始补足经历段数 =====")
|
||||||
|
|
||||||
|
// 重新获取页面所有大标题(用于确定范围边界)
|
||||||
|
const firstLocated = locateResults.find((r) => r.titleElement)
|
||||||
|
if (!firstLocated || !firstLocated.titleElement) {
|
||||||
|
console.log(" ❌ 无经历大标题,跳过补足")
|
||||||
|
console.log("===== OfferPie: 经历段数补足完毕 =====")
|
||||||
|
return locateResults
|
||||||
|
}
|
||||||
|
const refSignature = extractTitleSignature(firstLocated.titleElement)
|
||||||
|
let allPageTitles = findAllTitlesWithSameSignature(document.body, refSignature)
|
||||||
|
|
||||||
|
for (const result of locateResults) {
|
||||||
|
if (!result.titleElement) continue
|
||||||
|
|
||||||
|
// 获取简历数据中该经历的段数
|
||||||
|
const resumeCount = (resumeData[result.section] as unknown[])?.length || 0
|
||||||
|
if (resumeCount <= result.expandedCount) {
|
||||||
|
console.log(` [${result.section}] 简历${resumeCount}段 ≤ 页面${result.expandedCount}段,无需添加`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const needAdd = resumeCount - result.expandedCount
|
||||||
|
console.log(` [${result.section}] 简历${resumeCount}段 > 页面${result.expandedCount}段,需添加 ${needAdd} 段`)
|
||||||
|
|
||||||
|
// 找到对应的 config
|
||||||
|
const config = EXPERIENCE_SECTION_CONFIGS.find((c) => c.section === result.section)
|
||||||
|
if (!config) continue
|
||||||
|
|
||||||
|
// 逐次点击添加按钮
|
||||||
|
for (let addIdx = 0; addIdx < needAdd; addIdx++) {
|
||||||
|
// 每次点击前重新统计当前段数,确认是否真的还需要添加
|
||||||
|
allPageTitles = findAllTitlesWithSameSignature(document.body, refSignature)
|
||||||
|
const checkIdx = allPageTitles.findIndex((pt) => pt.element === result.titleElement)
|
||||||
|
const checkNextEl = checkIdx >= 0 && checkIdx < allPageTitles.length - 1 ? allPageTitles[checkIdx + 1].element : null
|
||||||
|
const currentResult = countExpandedInRange(result.titleElement!, checkNextEl, config, lang)
|
||||||
|
result.expandedCount = currentResult.count
|
||||||
|
|
||||||
|
if (result.expandedCount >= resumeCount) {
|
||||||
|
console.log(` ✅ 当前段数${result.expandedCount}已达到简历段数${resumeCount},无需继续添加`)
|
||||||
|
// 更新 segmentRanges
|
||||||
|
result.segmentRanges = currentResult.matchedElements.map((el, i) => ({
|
||||||
|
startElement: el,
|
||||||
|
startText: el.textContent?.trim() || "",
|
||||||
|
isNewlyAdded: i >= (resumeCount - needAdd), // 超出初始段数的都标记为新增
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// 每次点击前重新获取范围边界(因为 DOM 可能已变化)
|
||||||
|
allPageTitles = findAllTitlesWithSameSignature(document.body, refSignature)
|
||||||
|
const idx = allPageTitles.findIndex((pt) => pt.element === result.titleElement)
|
||||||
|
const nextTitleEl = idx >= 0 && idx < allPageTitles.length - 1 ? allPageTitles[idx + 1].element : null
|
||||||
|
|
||||||
|
// 查找添加按钮(管辖范围内从上到下第一个)
|
||||||
|
const addBtn = findAddButton(result.titleElement, nextTitleEl, config, lang)
|
||||||
|
if (!addBtn) {
|
||||||
|
console.log(` ❌ 第${addIdx + 1}次:未找到添加按钮,停止`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` → 第${addIdx + 1}次:找到添加按钮 "${addBtn.textContent?.trim()}",点击中...`)
|
||||||
|
|
||||||
|
// 记录点击前的段数
|
||||||
|
const beforeCount = countExpandedInRange(result.titleElement, nextTitleEl, config, lang).count
|
||||||
|
|
||||||
|
// 点击添加按钮(逐层点击,检测到段数增加立即停止)
|
||||||
|
await clickAddButton(addBtn, result.titleElement, nextTitleEl, config, lang)
|
||||||
|
await delay(300) // 额外等待确保 DOM 稳定
|
||||||
|
|
||||||
|
// 重新获取范围边界并统计段数
|
||||||
|
allPageTitles = findAllTitlesWithSameSignature(document.body, refSignature)
|
||||||
|
const newIdx = allPageTitles.findIndex((pt) => pt.element === result.titleElement)
|
||||||
|
const newNextTitleEl = newIdx >= 0 && newIdx < allPageTitles.length - 1 ? allPageTitles[newIdx + 1].element : null
|
||||||
|
const afterResult = countExpandedInRange(result.titleElement, newNextTitleEl, config, lang)
|
||||||
|
const afterCount = afterResult.count
|
||||||
|
|
||||||
|
if (afterCount > beforeCount) {
|
||||||
|
console.log(` ✅ 第${addIdx + 1}次:段数 ${beforeCount} → ${afterCount},添加成功`)
|
||||||
|
// 记录新增段的信息
|
||||||
|
const newElements = afterResult.matchedElements.filter(
|
||||||
|
(el) => !result.segmentRanges.some((seg) => seg.startElement === el)
|
||||||
|
)
|
||||||
|
for (const newEl of newElements) {
|
||||||
|
result.segmentRanges.push({
|
||||||
|
startElement: newEl,
|
||||||
|
startText: newEl.textContent?.trim() || "",
|
||||||
|
isNewlyAdded: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
result.expandedCount = afterCount
|
||||||
|
// 点击后立即检查是否已达到目标段数
|
||||||
|
if (result.expandedCount >= resumeCount) {
|
||||||
|
console.log(` ✅ 当前段数${result.expandedCount}已达到简历段数${resumeCount},停止添加`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` ⚠️ 第${addIdx + 1}次:段数未变化 (${beforeCount} → ${afterCount}),可能点击无效`)
|
||||||
|
// 多等一会再试一次确认
|
||||||
|
await delay(500)
|
||||||
|
const retryResult = countExpandedInRange(result.titleElement, newNextTitleEl, config, lang)
|
||||||
|
if (retryResult.count > beforeCount) {
|
||||||
|
console.log(` ✅ 第${addIdx + 1}次(重试):段数 ${beforeCount} → ${retryResult.count},添加成功`)
|
||||||
|
const newElements = retryResult.matchedElements.filter(
|
||||||
|
(el) => !result.segmentRanges.some((seg) => seg.startElement === el)
|
||||||
|
)
|
||||||
|
for (const newEl of newElements) {
|
||||||
|
result.segmentRanges.push({
|
||||||
|
startElement: newEl,
|
||||||
|
startText: newEl.textContent?.trim() || "",
|
||||||
|
isNewlyAdded: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
result.expandedCount = retryResult.count
|
||||||
|
// 重试后也检查是否已达到目标
|
||||||
|
if (result.expandedCount >= resumeCount) {
|
||||||
|
console.log(` ✅ 当前段数${result.expandedCount}已达到简历段数${resumeCount},停止添加`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ 第${addIdx + 1}次:重试后段数仍未变化,停止添加`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印最终结果
|
||||||
|
console.log("===== OfferPie: 经历段数补足结果 =====")
|
||||||
|
for (const r of locateResults) {
|
||||||
|
const resumeCount = (resumeData[r.section] as unknown[])?.length || 0
|
||||||
|
console.log(
|
||||||
|
` [${r.section}] 简历数据${resumeCount}段 | 页面已展开${r.expandedCount}段` +
|
||||||
|
` | 段落信息: ${r.segmentRanges.map((s) => `"${s.startText}"${s.isNewlyAdded ? "(新增)" : ""}`).join(", ") || "无"}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
console.log("===== OfferPie: 经历段数补足完毕 =====")
|
||||||
|
|
||||||
|
return locateResults
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user