统一控制延时

This commit is contained in:
2026-07-30 15:29:24 +08:00
parent bfe13d300b
commit 66bca74267
11 changed files with 148 additions and 113 deletions
+13
View File
@@ -101,3 +101,16 @@ src/
- 禁止对 isAsyncDropdown 字段走 `tryClickDropdownListItem` 全页面搜(选项数可能为1,全页面搜会被其他同规格元素干扰) - 禁止对 isAsyncDropdown 字段走 `tryClickDropdownListItem` 全页面搜(选项数可能为1,全页面搜会被其他同规格元素干扰)
- `pickerFill.ts``groupBySpec` / `findVisibleListGroups` 的同规格标签组阈值为 ≥2(不可改回 ≥3,否则只有2个选项的下拉列表会漏匹配) - `pickerFill.ts``groupBySpec` / `findVisibleListGroups` 的同规格标签组阈值为 ≥2(不可改回 ≥3,否则只有2个选项的下拉列表会漏匹配)
- `clickBestOptionInDropdown` 是在指定弹出层 DOM 内递归搜所有可见叶子文字节点做模糊匹配的核心方法,**不可简化其递归逻辑** - `clickBestOptionInDropdown` 是在指定弹出层 DOM 内递归搜所有可见叶子文字节点做模糊匹配的核心方法,**不可简化其递归逻辑**
## 延时调用规范
- **项目中所有延时调用必须且只能使用 `src/utils/delay.ts` 中导出的 `delay` 函数**
- 禁止在任何文件中自行编写 `setTimeout` / `new Promise(resolve => setTimeout(...))` 等延时逻辑
- `delay` 函数接收延时等级参数,统一管理延时时长:
- `delay("low")` — 低延时(微等待,如点击后极短暂停)
- `delay("mid")` — 中延时(等待 DOM 更新、弹出层渲染等)
- `delay("high")` — 高延时(等待接口返回、搜索结果、动画完成等)
- `delay("max")` — 特殊超长延时(谨慎使用)
- 具体毫秒数只在 `src/utils/delay.ts` 中统一设置,调用方只使用等级名称
- 如需调整全局延时策略,只修改 `delay.ts` 中的 `DELAY_MS` 常量即可
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "offerpie-extension", "name": "offerpie-extension",
"displayName": "Offer派智能投递助手", "displayName": "Offer派智能投递助手",
"version": "0.1.0", "version": "0.1.1",
"description": "offer派平台旗下网申投递助手 - 自动化投递简历\n官网:https://offerpai.com.cn", "description": "offer派平台旗下网申投递助手 - 自动化投递简历\n官网:https://offerpai.com.cn",
"scripts": { "scripts": {
"dev": "plasmo dev", "dev": "plasmo dev",
+3 -3
View File
@@ -3,7 +3,7 @@
*/ */
/** 当前环境,手动切换 */ /** 当前环境,手动切换 */
const ENV = 'prod' const ENV = 'dev'
/** 各环境配置 */ /** 各环境配置 */
const envConfigs: Record<string, { const envConfigs: Record<string, {
@@ -20,14 +20,14 @@ const envConfigs: Record<string, {
// aiBaseApi: 'http://192.168.31.133:8000', // aiBaseApi: 'http://192.168.31.133:8000',
aiBaseApi: 'http://127.0.0.1:8000', aiBaseApi: 'http://127.0.0.1:8000',
cookieSourceUrl: 'http://127.0.0.1:5173', cookieSourceUrl: 'http://127.0.0.1:5173',
bridgeDomain: 'test.offerpai.com.cn', bridgeDomain: 'offerpai.com.cn',
pluginVersion: '0.1.1', pluginVersion: '0.1.1',
}, },
prod: { prod: {
dataBaseApi: 'https://www.offerpai.com.cn/api', dataBaseApi: 'https://www.offerpai.com.cn/api',
aiBaseApi: 'https://www.offerpai.com.cn/ai-api', aiBaseApi: 'https://www.offerpai.com.cn/ai-api',
cookieSourceUrl: 'https://www.offerpai.com.cn', cookieSourceUrl: 'https://www.offerpai.com.cn',
bridgeDomain: 'www.offerpai.com.cn', bridgeDomain: 'offerpai.com.cn',
pluginVersion: '0.1.1', pluginVersion: '0.1.1',
}, },
} }
+25 -24
View File
@@ -7,7 +7,8 @@
* 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。 * 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。
*/ */
import { fillMatchedField, delay, forceSetValue, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill" import { fillMatchedField, forceSetValue, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill"
import { delay } from "~utils/delay"
import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom" import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom"
import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher" import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
import { detectPickerField } from "~lib/pickerDetector" import { detectPickerField } from "~lib/pickerDetector"
@@ -103,7 +104,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
// const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E5%AE%BE%E5%A4%95%E6%B3%95%E5%B0%BC%E4%BA%9A%E5%A4%A7%E5%AD%A6_%E4%B8%81%E5%B1%B9%E6%B6%B5.pdf" // const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E5%AE%BE%E5%A4%95%E6%B3%95%E5%B0%BC%E4%BA%9A%E5%A4%A7%E5%AD%A6_%E4%B8%81%E5%B1%B9%E6%B6%B5.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("high") // 等待网站解析简历
// 4.5 定位经历区块并统计已展开段数 // 4.5 定位经历区块并统计已展开段数
const sectionResults = locateExperienceSections(document.body, lang) const sectionResults = locateExperienceSections(document.body, lang)
@@ -331,14 +332,14 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
const ok = await fillSearchPickerField(f) const ok = await fillSearchPickerField(f)
if (ok) { result.success++ } else { result.failed++ } 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 }) phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx })
await delay(100) await delay("mid")
continue continue
} else if (isBeisenCascadeField(f.labelText) && f.inputElement) { } else if (isBeisenCascadeField(f.labelText) && f.inputElement) {
// 北森多级级联选择器字段(地区、籍贯、民族等) // 北森多级级联选择器字段(地区、籍贯、民族等)
const ok = await fillBeisenCascadePicker(f.inputElement, f.labelText, f.fillValue) const ok = await fillBeisenCascadePicker(f.inputElement, f.labelText, f.fillValue)
if (ok) { result.success++ } else { result.failed++ } 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 }) phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx })
await delay(150) await delay("mid")
continue continue
} else { } else {
await detectPickerField(f, lang) await detectPickerField(f, lang)
@@ -353,7 +354,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
if (lastTextInput && !lastTextInputIsPicker) { if (lastTextInput && !lastTextInputIsPicker) {
;(lastTextInput as HTMLElement).click() ;(lastTextInput as HTMLElement).click()
lastTextInput.focus() lastTextInput.focus()
await delay(50) await delay("low")
lastTextInput.blur() lastTextInput.blur()
} else { } else {
if (document.activeElement instanceof HTMLElement) { if (document.activeElement instanceof HTMLElement) {
@@ -364,7 +365,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
} }
document.body.click() document.body.click()
} }
await delay(150) await delay("mid")
if (!f.isPicker && f.inputElement && !isTimePeriodField(f.key) && !isTimeSingleField(f.key)) { if (!f.isPicker && f.inputElement && !isTimePeriodField(f.key) && !isTimeSingleField(f.key)) {
lastTextInput = f.inputElement lastTextInput = f.inputElement
@@ -402,7 +403,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
console.log(` [${f.key}] "${f.labelText}" → 北森级联选择器 | fillValue: "${f.fillValue}"`) console.log(` [${f.key}] "${f.labelText}" → 北森级联选择器 | fillValue: "${f.fillValue}"`)
const ok = await fillBeisenCascadePicker(f.inputElement, f.labelText, f.fillValue) const ok = await fillBeisenCascadePicker(f.inputElement, f.labelText, f.fillValue)
if (ok) { result.success++ } else { result.failed++ } if (ok) { result.success++ } else { result.failed++ }
await delay(150) await delay("mid")
continue continue
} }
@@ -420,7 +421,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
if (lastTextInput) { if (lastTextInput) {
;(lastTextInput as HTMLElement).click() ;(lastTextInput as HTMLElement).click()
lastTextInput.focus() lastTextInput.focus()
await delay(50) await delay("low")
lastTextInput.blur() lastTextInput.blur()
} else { } else {
if (document.activeElement instanceof HTMLElement) { if (document.activeElement instanceof HTMLElement) {
@@ -431,7 +432,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
} }
document.body.click() document.body.click()
} }
await delay(150) await delay("mid")
if (!f.isPicker && f.inputElement) lastTextInput = f.inputElement if (!f.isPicker && f.inputElement) lastTextInput = f.inputElement
} }
@@ -1322,7 +1323,7 @@ async function b2FillDateTimeField(
// 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker // 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker
inputEl.focus() inputEl.focus()
;(inputEl as HTMLElement).click() ;(inputEl as HTMLElement).click()
await delay(150) await delay("mid")
const ok = await fillDatePicker(field) const ok = await fillDatePicker(field)
if (ok) { if (ok) {
@@ -1388,21 +1389,21 @@ async function b2FillBeisenRadioGroup(
const childEl = item.firstElementChild as HTMLElement | null const childEl = item.firstElementChild as HTMLElement | null
if (childEl) { if (childEl) {
childEl.click() childEl.click()
await delay(50) await delay("low")
// 子子级也点一下确保触发 // 子子级也点一下确保触发
const grandChildEl = childEl.firstElementChild as HTMLElement | null const grandChildEl = childEl.firstElementChild as HTMLElement | null
if (grandChildEl) { if (grandChildEl) {
grandChildEl.click() grandChildEl.click()
await delay(50) await delay("low")
} }
} else { } else {
// fallback:直接点击 radioItem 自身 // fallback:直接点击 radioItem 自身
;(item as HTMLElement).click() ;(item as HTMLElement).click()
await delay(50) await delay("low")
} }
// 4. 验证选中结果:检查子级是否出现 phoenix-radio--checked 类名 // 4. 验证选中结果:检查子级是否出现 phoenix-radio--checked 类名
await delay(100) await delay("mid")
const checkedEl = item.querySelector(".phoenix-radio--checked") const checkedEl = item.querySelector(".phoenix-radio--checked")
if (checkedEl) { if (checkedEl) {
// 选中成功,radio group 容器变 greenTwo // 选中成功,radio group 容器变 greenTwo
@@ -1499,7 +1500,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang) const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ } if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
} }
await delay(100) await delay("mid")
} }
} }
} else { } else {
@@ -1549,7 +1550,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
}).length }).length
const clicked = await clickAddButton(addBtn, titleEl, nextTitleEl, beforeCount) const clicked = await clickAddButton(addBtn, titleEl, nextTitleEl, beforeCount)
if (!clicked) { console.log(` [B2] "${sectionData.title}" 点击添加按钮无效`); break } if (!clicked) { console.log(` [B2] "${sectionData.title}" 点击添加按钮无效`); break }
await delay(150) await delay("mid")
} }
} }
@@ -1571,7 +1572,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang) const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ } if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
} }
await delay(100) await delay("mid")
} }
} }
} }
@@ -1600,7 +1601,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
const radioOk = await b2FillBeisenRadioGroup(field.label, field.value, titleEl, nextTitleEl) const radioOk = await b2FillBeisenRadioGroup(field.label, field.value, titleEl, nextTitleEl)
if (radioOk) { result.success++ } else { result.skipped++ } if (radioOk) { result.success++ } else { result.skipped++ }
} }
await delay(100) await delay("mid")
} }
} }
} }
@@ -1661,7 +1662,7 @@ export async function fillBeisenCascadePicker(
inputEl.scrollIntoView({ block: "center", behavior: "instant" }) inputEl.scrollIntoView({ block: "center", behavior: "instant" })
;(inputEl as HTMLElement).click() ;(inputEl as HTMLElement).click()
inputEl.focus() inputEl.focus()
await delay(400) await delay("high")
// 2. 在页面中找 common-unmodeled-layer 面板(取最后一个,即最新弹出的) // 2. 在页面中找 common-unmodeled-layer 面板(取最后一个,即最新弹出的)
const allPanels = document.querySelectorAll(".common-unmodeled-layer") const allPanels = document.querySelectorAll(".common-unmodeled-layer")
@@ -1695,7 +1696,7 @@ export async function fillBeisenCascadePicker(
const clearBtn = panelEl.querySelector(".clear-select-data") as HTMLElement | null const clearBtn = panelEl.querySelector(".clear-select-data") as HTMLElement | null
if (clearBtn) { if (clearBtn) {
clearBtn.click() clearBtn.click()
await delay(200) // 等待清空操作生效 await delay("mid") // 等待清空操作生效
console.log(`OfferPie: [北森级联] "${labelText}" 已清空已选内容`) console.log(`OfferPie: [北森级联] "${labelText}" 已清空已选内容`)
} }
@@ -1704,7 +1705,7 @@ export async function fillBeisenCascadePicker(
forceSetValue(searchInput, fillValue) forceSetValue(searchInput, fillValue)
searchInput.dispatchEvent(new Event("input", { bubbles: true })) searchInput.dispatchEvent(new Event("input", { bubbles: true }))
searchInput.dispatchEvent(new Event("change", { bubbles: true })) searchInput.dispatchEvent(new Event("change", { bubbles: true }))
await delay(300) // 等待搜索结果刷新(太短会导致选项列表未更新,选到错误选项) await delay("mid") // 等待搜索结果刷新(太短会导致选项列表未更新,选到错误选项)
// 6. 检测选项集合位置(两种情况) // 6. 检测选项集合位置(两种情况)
// 在 left-container 下查找 // 在 left-container 下查找
@@ -1728,7 +1729,7 @@ export async function fillBeisenCascadePicker(
const svgEl = iconSpan.querySelector("svg") as unknown as HTMLElement | null const svgEl = iconSpan.querySelector("svg") as unknown as HTMLElement | null
if (svgEl) svgEl.dispatchEvent(new MouseEvent("click", { bubbles: true })) if (svgEl) svgEl.dispatchEvent(new MouseEvent("click", { bubbles: true }))
optionClicked = true optionClicked = true
await delay(200) await delay("mid")
console.log(`OfferPie: [北森级联] "${labelText}" 已点击第一个选项的 icon-container + svg`) console.log(`OfferPie: [北森级联] "${labelText}" 已点击第一个选项的 icon-container + svg`)
} }
} }
@@ -1754,7 +1755,7 @@ export async function fillBeisenCascadePicker(
const svgEl = iconSpan.querySelector("svg") as unknown as HTMLElement | null const svgEl = iconSpan.querySelector("svg") as unknown as HTMLElement | null
if (svgEl) svgEl.dispatchEvent(new MouseEvent("click", { bubbles: true })) if (svgEl) svgEl.dispatchEvent(new MouseEvent("click", { bubbles: true }))
optionClicked = true optionClicked = true
await delay(200) await delay("mid")
console.log(`OfferPie: [北森级联] "${labelText}" 已点击第一个选项的 icon-container + svg`) console.log(`OfferPie: [北森级联] "${labelText}" 已点击第一个选项的 icon-container + svg`)
} }
} }
@@ -1822,7 +1823,7 @@ export async function fillBeisenCascadePicker(
} }
} }
await delay(300) await delay("mid")
console.log(`OfferPie: [北森级联] "${labelText}" 已点击确定按钮`) console.log(`OfferPie: [北森级联] "${labelText}" 已点击确定按钮`)
} else { } else {
console.log(`OfferPie: [北森级联] "${labelText}" 未找到确定按钮`) console.log(`OfferPie: [北森级联] "${labelText}" 未找到确定按钮`)
+13 -12
View File
@@ -7,7 +7,8 @@
* 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。 * 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。
*/ */
import { fillMatchedField, delay, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill" import { fillMatchedField, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill"
import { delay } from "~utils/delay"
import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom" import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom"
import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher" import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
import { detectPickerField } from "~lib/pickerDetector" import { detectPickerField } from "~lib/pickerDetector"
@@ -103,7 +104,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
// const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E5%AE%BE%E5%A4%95%E6%B3%95%E5%B0%BC%E4%BA%9A%E5%A4%A7%E5%AD%A6_%E4%B8%81%E5%B1%B9%E6%B6%B5.pdf" // const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E5%AE%BE%E5%A4%95%E6%B3%95%E5%B0%BC%E4%BA%9A%E5%A4%A7%E5%AD%A6_%E4%B8%81%E5%B1%B9%E6%B6%B5.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("high") // 等待网站解析简历
// 4.5 定位经历区块并统计已展开段数 // 4.5 定位经历区块并统计已展开段数
const sectionResults = locateExperienceSections(document.body, lang) const sectionResults = locateExperienceSections(document.body, lang)
@@ -331,7 +332,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
const ok = await fillSearchPickerField(f) const ok = await fillSearchPickerField(f)
if (ok) { result.success++ } else { result.failed++ } 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 }) phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx })
await delay(100) await delay("mid")
continue continue
} else { } else {
await detectPickerField(f, lang) await detectPickerField(f, lang)
@@ -346,7 +347,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
if (lastTextInput && !lastTextInputIsPicker) { if (lastTextInput && !lastTextInputIsPicker) {
;(lastTextInput as HTMLElement).click() ;(lastTextInput as HTMLElement).click()
lastTextInput.focus() lastTextInput.focus()
await delay(50) await delay("low")
lastTextInput.blur() lastTextInput.blur()
} else { } else {
if (document.activeElement instanceof HTMLElement) { if (document.activeElement instanceof HTMLElement) {
@@ -357,7 +358,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
} }
document.body.click() document.body.click()
} }
await delay(150) await delay("mid")
if (!f.isPicker && f.inputElement && !isTimePeriodField(f.key) && !isTimeSingleField(f.key)) { if (!f.isPicker && f.inputElement && !isTimePeriodField(f.key) && !isTimeSingleField(f.key)) {
lastTextInput = f.inputElement lastTextInput = f.inputElement
@@ -404,7 +405,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
if (lastTextInput) { if (lastTextInput) {
;(lastTextInput as HTMLElement).click() ;(lastTextInput as HTMLElement).click()
lastTextInput.focus() lastTextInput.focus()
await delay(50) await delay("low")
lastTextInput.blur() lastTextInput.blur()
} else { } else {
if (document.activeElement instanceof HTMLElement) { if (document.activeElement instanceof HTMLElement) {
@@ -415,7 +416,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
} }
document.body.click() document.body.click()
} }
await delay(150) await delay("mid")
if (!f.isPicker && f.inputElement) lastTextInput = f.inputElement if (!f.isPicker && f.inputElement) lastTextInput = f.inputElement
} }
@@ -1257,7 +1258,7 @@ async function b2FillDateTimeField(
// 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker // 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker
inputEl.focus() inputEl.focus()
;(inputEl as HTMLElement).click() ;(inputEl as HTMLElement).click()
await delay(150) await delay("mid")
const ok = await fillDatePicker(field) const ok = await fillDatePicker(field)
if (ok) { if (ok) {
@@ -1339,7 +1340,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang) const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ } if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
} }
await delay(100) await delay("mid")
} }
} }
} else { } else {
@@ -1389,7 +1390,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
}).length }).length
const clicked = await clickAddButton(addBtn, titleEl, nextTitleEl, beforeCount) const clicked = await clickAddButton(addBtn, titleEl, nextTitleEl, beforeCount)
if (!clicked) { console.log(` [B2] "${sectionData.title}" 点击添加按钮无效`); break } if (!clicked) { console.log(` [B2] "${sectionData.title}" 点击添加按钮无效`); break }
await delay(150) await delay("mid")
} }
} }
@@ -1407,7 +1408,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang) const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ } if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
} }
await delay(100) await delay("mid")
} }
} }
} }
@@ -1426,7 +1427,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang) const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ } if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
} }
await delay(100) await delay("mid")
} }
} }
} }
+35 -39
View File
@@ -7,14 +7,10 @@ import type { MatchedFormField } from "./types"
import { findAndClickOptionInVisiblePopups, clickBestOptionInDropdown } from "./pickerFill" import { findAndClickOptionInVisiblePopups, clickBestOptionInDropdown } from "./pickerFill"
import { fillDatePicker } from "./datePicker" import { fillDatePicker } from "./datePicker"
import { JOB_FORM_LABELS } from "./constants" import { JOB_FORM_LABELS } from "./constants"
import { delay } from "~utils/delay"
// ============ 工具函数 ============ // ============ 工具函数 ============
/** 延时工具函数 */
export function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/** /**
* 通用的值写入方法(适配各种前端框架的受控组件) * 通用的值写入方法(适配各种前端框架的受控组件)
* 优先用 execCommand 模拟真实输入,失败则 fallback 到原生 setter * 优先用 execCommand 模拟真实输入,失败则 fallback 到原生 setter
@@ -60,12 +56,12 @@ export function forceSetValue(el: HTMLInputElement | HTMLTextAreaElement, value:
/** 关闭弹出层 */ /** 关闭弹出层 */
export async function closePopup(inputEl: HTMLElement) { export async function closePopup(inputEl: HTMLElement) {
inputEl.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: 27, bubbles: true })) inputEl.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: 27, bubbles: true }))
await delay(100) await delay("mid")
inputEl.blur() inputEl.blur()
inputEl.dispatchEvent(new Event("blur", { bubbles: true })) inputEl.dispatchEvent(new Event("blur", { bubbles: true }))
await delay(100) await delay("mid")
document.body.click() document.body.click()
await delay(200) await delay("mid")
} }
// ============ 字段填写 ============ // ============ 字段填写 ============
@@ -103,7 +99,7 @@ export async function fillMatchedField(field: MatchedFormField): Promise<boolean
if (inputElement instanceof HTMLInputElement && inputElement.type === "file") { console.warn(`OfferPie: ⏭ "${labelText}" 是文件上传框,跳过`); return false } if (inputElement instanceof HTMLInputElement && inputElement.type === "file") { console.warn(`OfferPie: ⏭ "${labelText}" 是文件上传框,跳过`); return false }
if (isPicker) return await fillPickerField(field) if (isPicker) return await fillPickerField(field)
inputElement.focus() inputElement.focus()
await delay(50) await delay("low")
forceSetValue(inputElement, fillValue) forceSetValue(inputElement, fillValue)
console.log(`OfferPie: ✅ [输入框] 已填写 "${labelText}" = "${fillValue}"`) console.log(`OfferPie: ✅ [输入框] 已填写 "${labelText}" = "${fillValue}"`)
return true return true
@@ -167,9 +163,9 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
// 搜不到时再补充搜索页面上其他可见弹出层(处理嵌套容器),最终都搜不到才关闭后走步骤1-4。 // 搜不到时再补充搜索页面上其他可见弹出层(处理嵌套容器),最终都搜不到才关闭后走步骤1-4。
if (!isDateValue && field.pickerDropdownElement) { if (!isDateValue && field.pickerDropdownElement) {
const existingPopupEl = field.pickerDropdownElement const existingPopupEl = field.pickerDropdownElement
await delay(100) // 等待弹出层渲染完成 await delay("mid") // 等待弹出层渲染完成
if (clickBestOptionInDropdown(existingPopupEl, fillValue)) { if (clickBestOptionInDropdown(existingPopupEl, fillValue)) {
await delay(300); await closePopup(inputElement) await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}" (已有弹出层)`) console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}" (已有弹出层)`)
return true return true
} }
@@ -181,7 +177,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
const htmlEl = el as HTMLElement const htmlEl = el as HTMLElement
if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0 && htmlEl !== existingPopupEl) { if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0 && htmlEl !== existingPopupEl) {
if (clickBestOptionInDropdown(htmlEl, fillValue)) { if (clickBestOptionInDropdown(htmlEl, fillValue)) {
await delay(300); await closePopup(inputElement) await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}" (嵌套弹出层)`) console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}" (嵌套弹出层)`)
return true return true
} }
@@ -189,7 +185,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
} }
// 仍然没找到,关闭弹出层后走步骤1-4 重新展开 // 仍然没找到,关闭弹出层后走步骤1-4 重新展开
await closePopup(inputElement) await closePopup(inputElement)
await delay(200) await delay("mid")
} }
// ---- 步骤1:记录点击前的 DOM 快照 ---- // ---- 步骤1:记录点击前的 DOM 快照 ----
@@ -218,7 +214,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
clickTargets[i].click() clickTargets[i].click()
} }
inputElement.focus() inputElement.focus()
await delay(300) await delay("mid")
if (isDateValue) return await fillDatePicker(field) if (isDateValue) return await fillDatePicker(field)
@@ -266,7 +262,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
if (dropdownEl) { if (dropdownEl) {
console.log(`OfferPie: [fillPicker] "${labelText}" 找到弹出层: ${dropdownEl.tagName}.${(dropdownEl.className || "").toString().split(" ")[0]}`) console.log(`OfferPie: [fillPicker] "${labelText}" 找到弹出层: ${dropdownEl.tagName}.${(dropdownEl.className || "").toString().split(" ")[0]}`)
if (clickBestOptionInDropdown(dropdownEl, fillValue)) { if (clickBestOptionInDropdown(dropdownEl, fillValue)) {
await delay(300); await closePopup(inputElement) await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`) console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true return true
} }
@@ -274,23 +270,23 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
// ---- 步骤5fallback - 在弹出层中搜索(有弹出层在弹出层内搜,没有则全页面搜) ---- // ---- 步骤5fallback - 在弹出层中搜索(有弹出层在弹出层内搜,没有则全页面搜) ----
if (findAndClickOptionInVisiblePopups(fillValue, labelText, dropdownEl)) { if (findAndClickOptionInVisiblePopups(fillValue, labelText, dropdownEl)) {
await delay(300); await closePopup(inputElement) await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`) console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true return true
} }
// ---- 步骤6:写入值触发搜索,再尝试一次 ---- // ---- 步骤6:写入值触发搜索,再尝试一次 ----
forceSetValue(inputElement, fillValue) forceSetValue(inputElement, fillValue)
await delay(500) await delay("high")
if (findAndClickOptionInVisiblePopups(fillValue, labelText, dropdownEl)) { if (findAndClickOptionInVisiblePopups(fillValue, labelText, dropdownEl)) {
await delay(300); await closePopup(inputElement) await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`) console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true return true
} }
// ---- 步骤7:兜底 Enter 确认 ---- // ---- 步骤7:兜底 Enter 确认 ----
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true })) inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
await delay(300) await delay("mid")
await closePopup(inputElement) await closePopup(inputElement)
console.log(`OfferPie: ⚠️ [选择器] 未找到匹配选项,已兜底填写 "${labelText}" = "${fillValue}"`) console.log(`OfferPie: ⚠️ [选择器] 未找到匹配选项,已兜底填写 "${labelText}" = "${fillValue}"`)
return true return true
@@ -305,7 +301,7 @@ export async function fillAllFields(fields: MatchedFormField[]): Promise<{ total
if (field.inputType !== "radio" && !field.inputElement) { result.skipped++; continue } if (field.inputType !== "radio" && !field.inputElement) { result.skipped++; continue }
const ok = await fillMatchedField(field) const ok = await fillMatchedField(field)
if (ok) result.success++; else result.failed++ if (ok) result.success++; else result.failed++
await delay(200) await delay("mid")
} }
console.log(`===== OfferPie: 自动填写完毕 总计 ${result.total} | 成功 ${result.success} | 失败 ${result.failed} | 跳过 ${result.skipped} =====`) console.log(`===== OfferPie: 自动填写完毕 总计 ${result.total} | 成功 ${result.success} | 失败 ${result.failed} | 跳过 ${result.skipped} =====`)
return result return result
@@ -353,7 +349,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
// 2. 点击 input 获取焦点并输入值 // 2. 点击 input 获取焦点并输入值
inputElement.focus() inputElement.focus()
;(inputElement as HTMLElement).click() ;(inputElement as HTMLElement).click()
await delay(200) await delay("mid")
forceSetValue(inputElement, fillValue) forceSetValue(inputElement, fillValue)
// 3. 等待弹出层出现(异步下拉字段轮询等待,普通字段固定等待) // 3. 等待弹出层出现(异步下拉字段轮询等待,普通字段固定等待)
@@ -363,7 +359,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
// 接口异步下拉:轮询等待弹出层出现,最多等3秒 // 接口异步下拉:轮询等待弹出层出现,最多等3秒
console.log(`OfferPie: [搜索选择器] "${labelText}" 异步下拉字段,等待接口返回...`) console.log(`OfferPie: [搜索选择器] "${labelText}" 异步下拉字段,等待接口返回...`)
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
await delay(200) await delay("mid")
// 检查是否有新弹出层 // 检查是否有新弹出层
for (const child of Array.from(document.body.children)) { for (const child of Array.from(document.body.children)) {
if (!beforeBodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) { if (!beforeBodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) {
@@ -385,7 +381,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
if (hasNewPopup) break if (hasNewPopup) break
} }
} else { } else {
await delay(401) await delay("high")
// 检查 body 下新增的子元素 // 检查 body 下新增的子元素
for (const child of Array.from(document.body.children)) { for (const child of Array.from(document.body.children)) {
if (!beforeBodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) { if (!beforeBodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) {
@@ -414,19 +410,19 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
console.log(`OfferPie: ✅ [搜索选择器→普通输入] "${labelText}" 无弹出层,已直接填写 "${fillValue}"`) console.log(`OfferPie: ✅ [搜索选择器→普通输入] "${labelText}" 无弹出层,已直接填写 "${fillValue}"`)
inputElement.dispatchEvent(new Event("blur", { bubbles: true })) inputElement.dispatchEvent(new Event("blur", { bubbles: true }))
// 调试:blur 后再验证 // 调试:blur 后再验证
await delay(50) await delay("low")
console.log(`OfferPie: [搜索选择器→普通输入] "${labelText}" blur后验证: value="${inputElement.value}"`) console.log(`OfferPie: [搜索选择器→普通输入] "${labelText}" blur后验证: value="${inputElement.value}"`)
return true return true
} }
// 5. 有弹出层 → 走搜索选择器逻辑,等待搜索结果 // 5. 有弹出层 → 走搜索选择器逻辑,等待搜索结果
await delay(500) await delay("high")
// 【注意】对异步下拉字段,必须优先在检测到的弹出层元素内直接做文字匹配 // 【注意】对异步下拉字段,必须优先在检测到的弹出层元素内直接做文字匹配
// 不能走 tryClickDropdownListItem 全页面搜——异步下拉可能只有1-2个选项,全页面搜会被其他同规格元素干扰 // 不能走 tryClickDropdownListItem 全页面搜——异步下拉可能只有1-2个选项,全页面搜会被其他同规格元素干扰
if (isAsyncDropdown && newPopupEl) { if (isAsyncDropdown && newPopupEl) {
if (clickBestOptionInDropdown(newPopupEl, fillValue)) { if (clickBestOptionInDropdown(newPopupEl, fillValue)) {
await delay(300) await delay("mid")
await closePopup(inputElement) await closePopup(inputElement)
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"`) console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true return true
@@ -436,16 +432,16 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
// 在弹出层中查找匹配选项 // 在弹出层中查找匹配选项
const { findAndClickOptionInVisiblePopups } = await import("./pickerFill") const { findAndClickOptionInVisiblePopups } = await import("./pickerFill")
if (findAndClickOptionInVisiblePopups(fillValue, labelText, newPopupEl)) { if (findAndClickOptionInVisiblePopups(fillValue, labelText, newPopupEl)) {
await delay(300) await delay("mid")
await closePopup(inputElement) await closePopup(inputElement)
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"`) console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true return true
} }
// 6. 再等一会重试(有些网站接口慢) // 6. 再等一会重试(有些网站接口慢)
await delay(600) await delay("high")
if (findAndClickOptionInVisiblePopups(fillValue, labelText, newPopupEl)) { if (findAndClickOptionInVisiblePopups(fillValue, labelText, newPopupEl)) {
await delay(300) await delay("mid")
await closePopup(inputElement) await closePopup(inputElement)
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"(重试)`) console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"(重试)`)
return true return true
@@ -522,7 +518,7 @@ async function fillYearMonthPicker(inputEl: HTMLInputElement | HTMLTextAreaEleme
// 点击 input 展开下拉 // 点击 input 展开下拉
inputEl.focus() inputEl.focus()
;(inputEl as HTMLElement).click() ;(inputEl as HTMLElement).click()
await delay(300) await delay("mid")
// 检测新出现的弹出层 // 检测新出现的弹出层
let dropdownEl: HTMLElement | null = null let dropdownEl: HTMLElement | null = null
@@ -551,14 +547,14 @@ async function fillYearMonthPicker(inputEl: HTMLInputElement | HTMLTextAreaEleme
if (dropdownEl) { if (dropdownEl) {
// 在弹出层中精确匹配选项并点击 // 在弹出层中精确匹配选项并点击
if (clickBestOptionInDropdown(dropdownEl, value)) { if (clickBestOptionInDropdown(dropdownEl, value)) {
await delay(200) await delay("mid")
return true return true
} }
} }
// 兜底:直接写入值 // 兜底:直接写入值
forceSetValue(inputEl, value) forceSetValue(inputEl, value)
await delay(100) await delay("mid")
return true return true
} }
@@ -630,10 +626,10 @@ export async function fillTimePeriodField(
usedInputs.add(yearMonthInputs[3]) usedInputs.add(yearMonthInputs[3])
} }
await delay(200) await delay("mid")
// 点击空白关闭弹窗 // 点击空白关闭弹窗
document.body.click() document.body.click()
await delay(100) await delay("mid")
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${start.year}.${start.month} - ${end.year}.${end.month}`) console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${start.year}.${start.month} - ${end.year}.${end.month}`)
return true return true
@@ -649,7 +645,7 @@ export async function fillTimePeriodField(
usedInputs.add(yearMonthInputs[1]) usedInputs.add(yearMonthInputs[1])
} }
document.body.click() document.body.click()
await delay(100) await delay("mid")
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${start.year}.${start.month}`) console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${start.year}.${start.month}`)
return true return true
} }
@@ -671,14 +667,14 @@ export async function fillTimePeriodField(
} }
startInput.focus() startInput.focus()
;(startInput as HTMLElement).click() ;(startInput as HTMLElement).click()
await delay(300) await delay("mid")
const startOk = await fillDatePicker(startField) const startOk = await fillDatePicker(startField)
if (startOk) { if (startOk) {
usedInputs.add(startInput) usedInputs.add(startInput)
console.log(`OfferPie: [时间选择器] "${field.labelText}" 开始时间填写成功: ${startDate}`) console.log(`OfferPie: [时间选择器] "${field.labelText}" 开始时间填写成功: ${startDate}`)
} }
await delay(500) await delay("high")
// 从开始时间 input 往后找最近的下一个 input 作为结束时间 // 从开始时间 input 往后找最近的下一个 input 作为结束时间
if (endDate) { if (endDate) {
@@ -703,7 +699,7 @@ export async function fillTimePeriodField(
} }
endInput.focus() endInput.focus()
;(endInput as HTMLElement).click() ;(endInput as HTMLElement).click()
await delay(300) await delay("mid")
const endOk = await fillDatePicker(endField) const endOk = await fillDatePicker(endField)
if (endOk) { if (endOk) {
usedInputs.add(endInput) usedInputs.add(endInput)
@@ -768,7 +764,7 @@ export async function fillTimeSingleField(
usedInputs.add(yearMonthInputs[1]) usedInputs.add(yearMonthInputs[1])
} }
document.body.click() document.body.click()
await delay(100) await delay("mid")
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${year}.${month}`) console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${year}.${month}`)
return true return true
} }
+27 -26
View File
@@ -14,7 +14,8 @@
*/ */
import type { MatchedFormField } from "./types" import type { MatchedFormField } from "./types"
import { delay, forceSetValue, closePopup } from "./autofill" import { forceSetValue, closePopup } from "./autofill"
import { delay } from "~utils/delay"
// ============ 常量 ============ // ============ 常量 ============
@@ -560,7 +561,7 @@ async function detectNavButtons(panel: CalendarPanelInfo): Promise<NavButtons> {
// 点击按钮 // 点击按钮
btn.click() btn.click()
await delay(150) await delay("mid")
const afterYear = readCurrentYear(panel) const afterYear = readCurrentYear(panel)
const afterMonth = readCurrentMonth(panel) const afterMonth = readCurrentMonth(panel)
@@ -626,21 +627,21 @@ async function detectNavButtons(panel: CalendarPanelInfo): Promise<NavButtons> {
if (restoreBtn) { if (restoreBtn) {
restoreBtn.click() restoreBtn.click()
await delay(150) await delay("mid")
} else { } else {
// 没有已知反向按钮,尝试用位置对称的按钮恢复 // 没有已知反向按钮,尝试用位置对称的按钮恢复
// 如果当前按钮在前半部分,对称按钮在后半部分 // 如果当前按钮在前半部分,对称按钮在后半部分
const symmetricIdx = buttons.length - 1 - i const symmetricIdx = buttons.length - 1 - i
if (symmetricIdx !== i && symmetricIdx >= 0 && symmetricIdx < buttons.length) { if (symmetricIdx !== i && symmetricIdx >= 0 && symmetricIdx < buttons.length) {
buttons[symmetricIdx].click() buttons[symmetricIdx].click()
await delay(150) await delay("mid")
// 验证是否恢复 // 验证是否恢复
const checkYear = readCurrentYear(panel) const checkYear = readCurrentYear(panel)
const checkMonth = readCurrentMonth(panel) const checkMonth = readCurrentMonth(panel)
if (checkYear !== beforeYear || checkMonth !== beforeMonth) { if (checkYear !== beforeYear || checkMonth !== beforeMonth) {
// 没恢复成功,再点一次原按钮的对称位置 // 没恢复成功,再点一次原按钮的对称位置
buttons[symmetricIdx].click() buttons[symmetricIdx].click()
await delay(150) await delay("mid")
} }
} }
} }
@@ -684,7 +685,7 @@ async function restoreToDate(
if (yearBtn) { if (yearBtn) {
for (let i = 0; i < Math.abs(yearDiff) && i < 50; i++) { for (let i = 0; i < Math.abs(yearDiff) && i < 50; i++) {
yearBtn.click() yearBtn.click()
await delay(80) await delay("low")
} }
} }
} }
@@ -700,7 +701,7 @@ async function restoreToDate(
if (monthBtn) { if (monthBtn) {
for (let i = 0; i < Math.abs(monthDiff) && i < 24; i++) { for (let i = 0; i < Math.abs(monthDiff) && i < 24; i++) {
monthBtn.click() monthBtn.click()
await delay(80) await delay("low")
} }
} }
} }
@@ -731,7 +732,7 @@ async function navigateToYearMonth(
console.log(`OfferPie: [datePicker] 年份需要${yearDiff > 0 ? "增加" : "减少"} ${steps}`) console.log(`OfferPie: [datePicker] 年份需要${yearDiff > 0 ? "增加" : "减少"} ${steps}`)
for (let i = 0; i < steps && i < 100; i++) { for (let i = 0; i < steps && i < 100; i++) {
yearBtn.click() yearBtn.click()
await delay(100) await delay("mid")
// 验证年份确实变了 // 验证年份确实变了
const newYear = readCurrentYear(panel) const newYear = readCurrentYear(panel)
if (newYear === targetYear) break if (newYear === targetYear) break
@@ -744,7 +745,7 @@ async function navigateToYearMonth(
console.log(`OfferPie: [datePicker] 无年份按钮,用月份按钮跨 ${Math.abs(totalMonthDiff)} 个月`) console.log(`OfferPie: [datePicker] 无年份按钮,用月份按钮跨 ${Math.abs(totalMonthDiff)} 个月`)
for (let i = 0; i < Math.abs(totalMonthDiff) && i < 200; i++) { for (let i = 0; i < Math.abs(totalMonthDiff) && i < 200; i++) {
monthBtn.click() monthBtn.click()
await delay(60) await delay("low")
} }
} }
} }
@@ -770,7 +771,7 @@ async function navigateToYearMonth(
console.log(`OfferPie: [datePicker] 月份需要${monthDiff > 0 ? "增加" : "减少"} ${steps}`) console.log(`OfferPie: [datePicker] 月份需要${monthDiff > 0 ? "增加" : "减少"} ${steps}`)
for (let i = 0; i < steps && i < 24; i++) { for (let i = 0; i < steps && i < 24; i++) {
btn.click() btn.click()
await delay(100) await delay("mid")
const newMonth = readCurrentMonth(panel) const newMonth = readCurrentMonth(panel)
if (newMonth === targetMonth) break if (newMonth === targetMonth) break
} }
@@ -887,7 +888,7 @@ async function tryFillMonthPanel(
console.log(`OfferPie: [datePicker] 月份面板年份导航: ${yearDiff > 0 ? "+" : ""}${yearDiff}`) console.log(`OfferPie: [datePicker] 月份面板年份导航: ${yearDiff > 0 ? "+" : ""}${yearDiff}`)
for (let i = 0; i < steps && i < 50; i++) { for (let i = 0; i < steps && i < 50; i++) {
yearBtn.click() yearBtn.click()
await delay(150) await delay("mid")
const newYear = readCurrentYear(tempPanel) const newYear = readCurrentYear(tempPanel)
if (newYear === targetYear) break if (newYear === targetYear) break
} }
@@ -901,7 +902,7 @@ async function tryFillMonthPanel(
const steps = Math.abs(yearDiff) const steps = Math.abs(yearDiff)
for (let i = 0; i < steps && i < 50; i++) { for (let i = 0; i < steps && i < 50; i++) {
btn.click() btn.click()
await delay(150) await delay("mid")
const newYear = readCurrentYear(tempPanel) const newYear = readCurrentYear(tempPanel)
if (newYear === targetYear) break if (newYear === targetYear) break
} }
@@ -926,9 +927,9 @@ async function tryFillMonthPanel(
if (inner && isVisible(inner)) deepest = inner as HTMLElement if (inner && isVisible(inner)) deepest = inner as HTMLElement
deepest.click() deepest.click()
await delay(100) await delay("mid")
if (deepest !== cell) cell.click() if (deepest !== cell) cell.click()
await delay(300) await delay("mid")
await closePopup(inputElement) await closePopup(inputElement)
console.log(`OfferPie: ✅ [月份选择器] 已选择 "${labelText}" = "${fillValue}" (${targetYear}${targetMonth}月)`) console.log(`OfferPie: ✅ [月份选择器] 已选择 "${labelText}" = "${fillValue}" (${targetYear}${targetMonth}月)`)
return true return true
@@ -1059,12 +1060,12 @@ async function clickDayCell(
// 三层点击:叶子 → 父级 → 原始元素 // 三层点击:叶子 → 父级 → 原始元素
deepest.click() deepest.click()
await delay(50) await delay("low")
if (deepest.parentElement) deepest.parentElement.click() if (deepest.parentElement) deepest.parentElement.click()
await delay(50) await delay("low")
if (cell !== deepest && cell !== deepest.parentElement) (cell as HTMLElement).click() if (cell !== deepest && cell !== deepest.parentElement) (cell as HTMLElement).click()
await delay(300) await delay("mid")
await closePopup(inputElement) await closePopup(inputElement)
console.log(`OfferPie: ✅ [日期] 已选择 "${labelText}" = "${fillValue}" (日期 ${targetDay})`) console.log(`OfferPie: ✅ [日期] 已选择 "${labelText}" = "${fillValue}" (日期 ${targetDay})`)
return true return true
@@ -1158,9 +1159,9 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
console.warn(`OfferPie: [datePicker] 无法解析日期值: "${fillValue}"`) console.warn(`OfferPie: [datePicker] 无法解析日期值: "${fillValue}"`)
// 兜底:直接写入值 + Enter // 兜底:直接写入值 + Enter
forceSetValue(inputElement, fillValue) forceSetValue(inputElement, fillValue)
await delay(300) await delay("mid")
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true })) inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
await delay(300) await delay("mid")
await closePopup(inputElement as HTMLElement) await closePopup(inputElement as HTMLElement)
return true return true
} }
@@ -1170,7 +1171,7 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
// ---- 步骤2:写入日期字符串(触发面板联动) ---- // ---- 步骤2:写入日期字符串(触发面板联动) ----
forceSetValue(inputElement, fillValue) forceSetValue(inputElement, fillValue)
await delay(500) await delay("high")
// ---- 步骤2.5:如果只有年月(day=0),检测是否为月份面板 ---- // ---- 步骤2.5:如果只有年月(day=0),检测是否为月份面板 ----
if (targetDay === 0) { if (targetDay === 0) {
@@ -1208,13 +1209,13 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
if (targetDay === 0) { if (targetDay === 0) {
// 只需要年月,不需要点日期格子。尝试点击月份面板中的月份 // 只需要年月,不需要点日期格子。尝试点击月份面板中的月份
// 如果导航成功了,可能面板已经关闭(某些组件选完月份自动关闭) // 如果导航成功了,可能面板已经关闭(某些组件选完月份自动关闭)
await delay(200) await delay("mid")
await closePopup(inputElement as HTMLElement) await closePopup(inputElement as HTMLElement)
console.log(`OfferPie: ✅ [日期] 已导航到 "${labelText}" = "${fillValue}" (${targetYear}${targetMonth}月)`) console.log(`OfferPie: ✅ [日期] 已导航到 "${labelText}" = "${fillValue}" (${targetYear}${targetMonth}月)`)
return true return true
} }
await delay(200) await delay("mid")
const clicked = await clickDayCell(panel, targetDay, labelText, fillValue, inputElement as HTMLElement) const clicked = await clickDayCell(panel, targetDay, labelText, fillValue, inputElement as HTMLElement)
if (clicked) return true if (clicked) return true
@@ -1250,10 +1251,10 @@ async function fallbackClickDay(
break break
} }
} }
deepest.click(); await delay(50) deepest.click(); await delay("low")
if (deepest.parentElement) deepest.parentElement.click(); await delay(50) if (deepest.parentElement) deepest.parentElement.click(); await delay("low")
if (cell !== deepest && cell !== deepest.parentElement) (cell as HTMLElement).click() if (cell !== deepest && cell !== deepest.parentElement) (cell as HTMLElement).click()
await delay(300) await delay("mid")
await closePopup(inputElement) await closePopup(inputElement)
console.log(`OfferPie: ✅ [日期-兜底] 已选择 "${labelText}" = "${fillValue}" (日期 ${targetDay})`) console.log(`OfferPie: ✅ [日期-兜底] 已选择 "${labelText}" = "${fillValue}" (日期 ${targetDay})`)
return true return true
@@ -1263,7 +1264,7 @@ async function fallbackClickDay(
// 最终兜底:Enter 确认 // 最终兜底:Enter 确认
console.warn(`OfferPie: ⚠ [日期] "${labelText}" 未找到日历格子,Enter 确认`) console.warn(`OfferPie: ⚠ [日期] "${labelText}" 未找到日历格子,Enter 确认`)
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true })) inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
await delay(300) await delay("mid")
await closePopup(inputElement) await closePopup(inputElement)
return true return true
} }
+3 -3
View File
@@ -15,7 +15,7 @@
*/ */
import { EXPERIENCE_SECTION_CONFIGS, JOB_FORM_LABELS } from "./constants" import { EXPERIENCE_SECTION_CONFIGS, JOB_FORM_LABELS } from "./constants"
import { delay } from "./autofill" import { delay } from "~utils/delay"
import { snapshotElementsInRange, diffSnapshots, findTopLevelNewElements } from "./dom" import { snapshotElementsInRange, diffSnapshots, findTopLevelNewElements } from "./dom"
import type { ExperienceSection, ExperienceSectionConfig, ResumeData } from "./types" import type { ExperienceSection, ExperienceSectionConfig, ResumeData } from "./types"
@@ -766,7 +766,7 @@ export async function clickAddButton(
// 逐层点击,每层点击后检测 input 数量是否增加 // 逐层点击,每层点击后检测 input 数量是否增加
for (const target of clickTargets) { for (const target of clickTargets) {
target.click() target.click()
await delay(400) await delay("high")
const afterInputs = findInputsInRange(titleEl, nextTitleEl) const afterInputs = findInputsInRange(titleEl, nextTitleEl)
if (afterInputs.length > beforeInputCount) { if (afterInputs.length > beforeInputCount) {
@@ -1105,7 +1105,7 @@ export async function expandExperienceSections(
// 点击添加按钮 // 点击添加按钮
const clickSuccess = await clickAddButton(addBtn, result.titleElement, nextTitleEl, beforeInputCount) const clickSuccess = await clickAddButton(addBtn, result.titleElement, nextTitleEl, beforeInputCount)
await delay(300) await delay("mid")
if (clickSuccess) { if (clickSuccess) {
// 通过 DOM 差异找出新增的顶层容器(即一段经历的完整 DOM) // 通过 DOM 差异找出新增的顶层容器(即一段经历的完整 DOM)
+3 -3
View File
@@ -17,7 +17,7 @@
import type { MatchedFormField, UILibPickerConfig } from "./types" import type { MatchedFormField, UILibPickerConfig } from "./types"
import { UI_LIB_PICKER_CONFIGS } from "./constants" import { UI_LIB_PICKER_CONFIGS } from "./constants"
import { delay } from "./autofill" import { delay } from "~utils/delay"
/** 通过提示文字判断是否为选择器 */ /** 通过提示文字判断是否为选择器 */
function hasPickerHintText(field: MatchedFormField, lang: "zh" | "en"): boolean { function hasPickerHintText(field: MatchedFormField, lang: "zh" | "en"): boolean {
@@ -109,7 +109,7 @@ async function dismissPopup() {
document.activeElement.blur() document.activeElement.blur()
} }
document.body.click() document.body.click()
await delay(200) await delay("mid")
} }
/** /**
@@ -142,7 +142,7 @@ async function detectByClick(field: MatchedFormField): Promise<boolean> {
for (const target of clickTargets) { for (const target of clickTargets) {
const before = snapshotDOM() const before = snapshotDOM()
target.click() target.click()
await delay(200) await delay("mid")
if (hasNewPopup(before)) { if (hasNewPopup(before)) {
console.log(`OfferPie: [${field.key}] 点击 ${target.tagName}.${(target.className || "").toString().split(" ")[0]} 后检测到弹出层`) console.log(`OfferPie: [${field.key}] 点击 ${target.tagName}.${(target.className || "").toString().split(" ")[0]} 后检测到弹出层`)
+2 -2
View File
@@ -3,7 +3,7 @@
* 负责:检测页面上的简历上传按钮,从 OSS 下载简历文件并注入到 file input * 负责:检测页面上的简历上传按钮,从 OSS 下载简历文件并注入到 file input
*/ */
import { delay } from "./autofill" import { delay } from "~utils/delay"
/** /**
* 检测页面上的简历上传标签,并自动上传简历文件 * 检测页面上的简历上传标签,并自动上传简历文件
@@ -73,7 +73,7 @@ export async function detectAndUploadResume(resumeUrl: string): Promise<boolean>
} }
for (const target of clickTargets) { for (const target of clickTargets) {
target.click() target.click()
await delay(300) await delay("mid")
for (const input of Array.from(document.querySelectorAll('input[type="file"]'))) { for (const input of Array.from(document.querySelectorAll('input[type="file"]'))) {
const id = input.id?.toLowerCase() || "" const id = input.id?.toLowerCase() || ""
const name = input.getAttribute("name")?.toLowerCase() || "" const name = input.getAttribute("name")?.toLowerCase() || ""
+23
View File
@@ -0,0 +1,23 @@
/**
* 统一延时工具
* 所有延时调用必须通过此模块,禁止在其他文件中直接使用 setTimeout 或自定义 delay
*/
/** 延时等级 */
export type DelayLevel = "low" | "mid" | "high" | "max"
/** 各等级对应的毫秒数(统一在此处调整) */
const DELAY_MS: Record<DelayLevel, number> = {
low: 50, // <100ms 场景:微等待,点击后极短暂停
mid: 200, // 100~300ms 场景:等待 DOM 更新、弹出层渲染
high: 500, // >300ms 场景:等待接口返回、搜索结果、动画完成
max: 2000, // >2000ms 场景:特殊超长延时(谨慎使用)
}
/**
* 统一延时函数
* @param level 延时等级:low / mid / high / max
*/
export function delay(level: DelayLevel): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, DELAY_MS[level]))
}