重复填写添加经历段数校正、文档规范hook、调整填写速度

This commit is contained in:
2026-07-30 21:08:31 +08:00
parent 66bca74267
commit 8b996772ea
6 changed files with 161 additions and 15 deletions
+16
View File
@@ -0,0 +1,16 @@
{
"enabled": true,
"name": "Steering 文档同步提醒",
"description": "当 src/lib 目录下的文件被编辑时,提醒检查 .kiro/steering/project-guidelines.md 中的项目结构和规范说明是否需要同步更新",
"version": "1",
"when": {
"type": "fileEdited",
"patterns": [
"src/lib/*.ts"
]
},
"then": {
"type": "askAgent",
"prompt": "刚才修改了 lib 文件,请检查 .kiro/steering/project-guidelines.md 中的\"项目结构\"和相关规范章节是否需要同步更新(比如新增了方法、修改了核心逻辑、新增了文件等)。如果需要更新,提醒用户但不要自动修改。"
}
}
@@ -0,0 +1,16 @@
{
"enabled": true,
"name": "保护【注意】核心标记",
"description": "当 write 操作涉及 src/lib 文件时,检查是否有【注意】或【核心逻辑】标记被删除,防止核心逻辑被误删",
"version": "1",
"when": {
"type": "preToolUse",
"toolTypes": [
"write"
]
},
"then": {
"type": "askAgent",
"prompt": "即将对文件执行写入操作。如果目标文件在 src/lib/ 目录下,请检查本次写入是否会删除任何包含【注意】或【核心逻辑】或【核心】标记的注释行。如果会删除这些标记,必须停止操作并告知用户。这些标记保护的是经过大量调试验证的核心逻辑,不可擅自移除。"
}
}
+35 -10
View File
@@ -13,27 +13,44 @@ inclusion: always
``` ```
src/ src/
├── api/
│ ├── aiApi.ts # AI后端接口(Python后端,简历优化/AI生成等)
│ ├── dataApi.ts # 数据后端接口(Java后端,简历CRUD/岗位/会员等)
│ └── request.ts # 请求封装(axios实例、拦截器、Token注入)
├── background/ ├── background/
│ └── index.ts # 插件后台脚本(Service Worker │ └── index.ts # 插件后台脚本(Service Worker
├── components/ ├── components/
│ ├── SidebarPanel.tsx # 侧边栏面板组件(主操作界面,自动填写入口) │ ├── SidebarPanel.tsx # 侧边栏面板组件(主操作界面,自动填写入口,字段进度展示
│ └── SidebarPanel.scss # 侧边栏样式 │ └── SidebarPanel.scss # 侧边栏样式
├── contents/ ├── contents/
│ └── sidebar.tsx # Content Script 入口(注入侧边栏到页面) │ └── sidebar.tsx # Content Script 入口(注入侧边栏到页面)
├── handlers/
│ ├── handleAutoFillCommon.ts # 通用模式自动填写(不依赖任何CSS类名,纯DOM差异对比适配所有网站)
│ └── handleAutoFillBeisen.ts # 北森模式自动填写(针对北森Phoenix UI的特殊处理,含级联选择器等)
├── lib/ ├── lib/
│ ├── types.ts # 所有共享类型定义(简历接口、表单标签、匹配字段、UI组件库配置) │ ├── types.ts # 所有共享类型定义(简历接口、表单标签、匹配字段、UI组件库配置、经历区块类型
│ ├── constants.ts # 常量数据(JOB_FORM_LABELS标签数组、TEST_FILL_DATA测试数据、UI_LIB_PICKER_CONFIGS组件库配置、SPECIAL_LABEL_PLACEHOLDERS特殊标签修正配置) │ ├── constants.ts # 常量数据(JOB_FORM_LABELS标签数组、EXPERIENCE_SECTION_CONFIGS经历配置、UI_LIB_PICKER_CONFIGS组件库配置)
│ ├── dom.ts # DOM工具(extractDomStructure提取DOM树、detectPageLanguage语言检测、isJobApplicationForm表单页判断、buildSelector选择器生成) │ ├── dom.ts # DOM工具(extractDomStructure提取DOM树、detectPageLanguage语言检测、isJobApplicationForm表单页判断、buildSelector选择器生成、snapshotElementsInRange/diffSnapshots/findTopLevelNewElements DOM快照差异对比
│ ├── formMatcher.ts # 表单字段匹配(matchFormFields在DOM中匹配标签对应的input、findNearestInput查找input、fixSpecialLabelInput特殊标签修正 │ ├── formMatcher.ts # 表单字段匹配(matchFormFields全页面匹配、matchFormFieldsInRange范围内匹配、matchMainFields非经历区域匹配、findNearestInput查找input
│ ├── labelFinder.ts # 标签定位(findLabelForInput通过form-item容器+DOM逆向遍历两遍策略识别input对应的标签文字)
│ ├── pickerDetector.ts # 选择器识别(detectPickerField检测字段是否为选择器类型,通过UI组件库类名/提示文字/主动点击DOM差异对比三种方式) │ ├── pickerDetector.ts # 选择器识别(detectPickerField检测字段是否为选择器类型,通过UI组件库类名/提示文字/主动点击DOM差异对比三种方式)
│ ├── pickerFill.ts # 选择器选项匹配与点击(fuzzyMatchScore模糊匹配、clickBestOptionInDropdown弹出层选项点击、findAndClickOptionInVisiblePopups全局弹出层搜索) │ ├── pickerFill.ts # 选择器选项匹配与点击(fuzzyMatchScore模糊匹配、clickBestOptionInDropdown弹出层选项点击、findAndClickOptionInVisiblePopups弹出层搜索)
│ ├── datePicker.ts # 日期选择器填写(fillDatePicker日期填写、tryClickMonth月份点击、tryClickDay日期点击 │ ├── datePicker.ts # 日期选择器填写(fillDatePicker日期填写、detectNavButtons按钮探测、navigateToYearMonth导航、tryFillMonthPanel月份面板
│ ├── autofill.ts # 自动填写核心(delay/forceSetValue工具函数、fillMatchedField字段填写入口、fillPickerField选择器填写含DOM差异对比、closePopup弹窗关闭、fillAllFields批量填写 │ ├── autofill.ts # 自动填写核心(forceSetValue强制写值、fillMatchedField统一填写入口、fillPickerField选择器填写含DOM差异对比、fillSearchPickerField搜索型选择器、fillTimePeriodField/fillTimeSingleField时间字段、fillYearMonthPicker年月选择器
│ ├── experienceSection.ts # 经历区块识别与段数管理(locateExperienceSections定位5大经历、expandExperienceSections补足段数、countExistingSegments段数校正、relocateSegmentContainer重定位、sortExperienceByTime时间排序)
│ ├── fillStats.ts # 填写统计(scanPageFields按大标题分组扫描所有字段状态、extractNonResumeFields提取非简历格式字段、printFieldStats打印统计)
│ ├── formStyle.ts # 表单样式(setFieldHighlight字段背景高亮、isRequiredField必填检测)
│ ├── resumeDataHelper.ts # 简历数据工具(getResumeFieldValue根据section/index/field获取简历值)
│ ├── resumeUpload.ts # 简历上传(detectAndUploadResume检测上传按钮并从OSS下载注入文件) │ ├── resumeUpload.ts # 简历上传(detectAndUploadResume检测上传按钮并从OSS下载注入文件)
│ └── api.ts # API接口模块 │ └── channelBridge.ts # 跨域通信桥(panelBridge跨域读写数据,用于Content Script和网页端通信)
├── utils/
│ ├── auth.ts # 认证工具(getMemberStatus会员状态查询、getCustomizeResume定制简历获取)
│ ├── cookie.ts # Cookie工具(getCookieValue从浏览器获取Cookie值)
│ ├── delay.ts # 延时工具(delay统一延时函数,按等级管理:low/mid/high/max
│ └── storage.ts # 存储工具(chrome.storage的get/set/remove封装)
└── assets/ └── assets/
├── icon.png # 插件图标 ├── icon.png # 插件图标
└── icon1.png # 备用图标 └── logo-offerpai.png # OfferPai logo
``` ```
## 编码规范 ## 编码规范
@@ -114,3 +131,11 @@ src/
- `delay("max")` — 特殊超长延时(谨慎使用) - `delay("max")` — 特殊超长延时(谨慎使用)
- 具体毫秒数只在 `src/utils/delay.ts` 中统一设置,调用方只使用等级名称 - 具体毫秒数只在 `src/utils/delay.ts` 中统一设置,调用方只使用等级名称
- 如需调整全局延时策略,只修改 `delay.ts` 中的 `DELAY_MS` 常量即可 - 如需调整全局延时策略,只修改 `delay.ts` 中的 `DELAY_MS` 常量即可
## 代码组织与可维护性规范
- **修改或添加新逻辑时,必须完整查看逻辑相关的所有引用和引入**,理解上下游调用关系后再动手
- **只要能实现,新逻辑优先单独抽出为独立方法**,通过返回值向调用方提供所需结果,保持项目的解耦性和高可维护性
- 禁止将新增逻辑直接内联到已有的大方法中导致方法膨胀、职责混乱
- 独立方法应有清晰的中文注释说明:用途、入参、返回值、安全退化行为
- 调用独立方法的位置需加注释说明为什么调用、结果如何使用
+90 -1
View File
@@ -17,6 +17,7 @@
import { EXPERIENCE_SECTION_CONFIGS, JOB_FORM_LABELS } from "./constants" import { EXPERIENCE_SECTION_CONFIGS, JOB_FORM_LABELS } from "./constants"
import { delay } from "~utils/delay" import { delay } from "~utils/delay"
import { snapshotElementsInRange, diffSnapshots, findTopLevelNewElements } from "./dom" import { snapshotElementsInRange, diffSnapshots, findTopLevelNewElements } from "./dom"
import { findNearestInput } from "./formMatcher"
import type { ExperienceSection, ExperienceSectionConfig, ResumeData } from "./types" 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 } 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段经历的边界 * 检测默认第1段经历的边界
* 在大标题到下一个大标题之间,找到第一个 input,然后向上找段落容器 * 在大标题到下一个大标题之间,找到第一个 input,然后向上找段落容器
@@ -1059,6 +1136,19 @@ export async function expandExperienceSections(
for (const result of locateResults) { for (const result of locateResults) {
if (!result.titleElement) continue 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 const resumeCount = (resumeData[result.section] as unknown[])?.length || 0
if (resumeCount <= result.expandedCount) { if (resumeCount <= result.expandedCount) {
console.log(` [${result.section}] 简历${resumeCount}段 ≤ 页面${result.expandedCount}段,无需添加`) console.log(` [${result.section}] 简历${resumeCount}段 ≤ 页面${result.expandedCount}段,无需添加`)
@@ -1068,7 +1158,6 @@ export async function expandExperienceSections(
const needAdd = resumeCount - result.expandedCount const needAdd = resumeCount - result.expandedCount
console.log(` [${result.section}] 简历${resumeCount}段 > 页面${result.expandedCount}段,需添加 ${needAdd}`) console.log(` [${result.section}] 简历${resumeCount}段 > 页面${result.expandedCount}段,需添加 ${needAdd}`)
const config = EXPERIENCE_SECTION_CONFIGS.find((c) => c.section === result.section)
if (!config) continue if (!config) continue
/** /**
+1 -1
View File
@@ -61,7 +61,7 @@ const INPUT_SEL = "input:not([type='hidden']):not([type='submit']):not([type='bu
* 在标签元素附近查找最近的、属于同一表单项的 input/textarea 输入框 * 在标签元素附近查找最近的、属于同一表单项的 input/textarea 输入框
* 策略:1.表单项容器内查找 → 2.逐级向上查找 → 3.兜底查兄弟节点 * 策略:1.表单项容器内查找 → 2.逐级向上查找 → 3.兜底查兄弟节点
*/ */
function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextAreaElement | null { export function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextAreaElement | null {
// 策略1:找标签所在的最近表单项容器 // 策略1:找标签所在的最近表单项容器
for (const sel of FORM_ITEM_SELECTORS) { for (const sel of FORM_ITEM_SELECTORS) {
const container = labelEl.closest(sel) const container = labelEl.closest(sel)
+2 -2
View File
@@ -8,8 +8,8 @@ export type DelayLevel = "low" | "mid" | "high" | "max"
/** 各等级对应的毫秒数(统一在此处调整) */ /** 各等级对应的毫秒数(统一在此处调整) */
const DELAY_MS: Record<DelayLevel, number> = { const DELAY_MS: Record<DelayLevel, number> = {
low: 50, // <100ms 场景:微等待,点击后极短暂停 low: 10, // <100ms 场景:微等待,点击后极短暂停
mid: 200, // 100~300ms 场景:等待 DOM 更新、弹出层渲染 mid: 50, // 100~300ms 场景:等待 DOM 更新、弹出层渲染
high: 500, // >300ms 场景:等待接口返回、搜索结果、动画完成 high: 500, // >300ms 场景:等待接口返回、搜索结果、动画完成
max: 2000, // >2000ms 场景:特殊超长延时(谨慎使用) max: 2000, // >2000ms 场景:特殊超长延时(谨慎使用)
} }