统一控制延时

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
+3 -3
View File
@@ -3,7 +3,7 @@
*/
/** 当前环境,手动切换 */
const ENV = 'prod'
const ENV = 'dev'
/** 各环境配置 */
const envConfigs: Record<string, {
@@ -20,14 +20,14 @@ const envConfigs: Record<string, {
// aiBaseApi: 'http://192.168.31.133:8000',
aiBaseApi: 'http://127.0.0.1:8000',
cookieSourceUrl: 'http://127.0.0.1:5173',
bridgeDomain: 'test.offerpai.com.cn',
bridgeDomain: 'offerpai.com.cn',
pluginVersion: '0.1.1',
},
prod: {
dataBaseApi: 'https://www.offerpai.com.cn/api',
aiBaseApi: 'https://www.offerpai.com.cn/ai-api',
cookieSourceUrl: 'https://www.offerpai.com.cn',
bridgeDomain: 'www.offerpai.com.cn',
bridgeDomain: 'offerpai.com.cn',
pluginVersion: '0.1.1',
},
}
+25 -24
View File
@@ -7,7 +7,8 @@
* 不要在此文件中自行编写选择器操作逻辑,必须引用 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 { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
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 uploaded = await detectAndUploadResume(resumeUrl)
// console.log(`===== OfferPie: 简历上传 ${uploaded ? "成功" : "跳过(未找到上传按钮或失败)"} =====`)
// if (uploaded) await delay(1000) // 等待网站解析简历
// if (uploaded) await delay("high") // 等待网站解析简历
// 4.5 定位经历区块并统计已展开段数
const sectionResults = locateExperienceSections(document.body, lang)
@@ -331,14 +332,14 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
const ok = await fillSearchPickerField(f)
if (ok) { result.success++ } else { result.failed++ }
phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx })
await delay(100)
await delay("mid")
continue
} else if (isBeisenCascadeField(f.labelText) && f.inputElement) {
// 北森多级级联选择器字段(地区、籍贯、民族等)
const ok = await fillBeisenCascadePicker(f.inputElement, f.labelText, f.fillValue)
if (ok) { result.success++ } else { result.failed++ }
phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx })
await delay(150)
await delay("mid")
continue
} else {
await detectPickerField(f, lang)
@@ -353,7 +354,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
if (lastTextInput && !lastTextInputIsPicker) {
;(lastTextInput as HTMLElement).click()
lastTextInput.focus()
await delay(50)
await delay("low")
lastTextInput.blur()
} else {
if (document.activeElement instanceof HTMLElement) {
@@ -364,7 +365,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
}
document.body.click()
}
await delay(150)
await delay("mid")
if (!f.isPicker && f.inputElement && !isTimePeriodField(f.key) && !isTimeSingleField(f.key)) {
lastTextInput = f.inputElement
@@ -402,7 +403,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
console.log(` [${f.key}] "${f.labelText}" → 北森级联选择器 | fillValue: "${f.fillValue}"`)
const ok = await fillBeisenCascadePicker(f.inputElement, f.labelText, f.fillValue)
if (ok) { result.success++ } else { result.failed++ }
await delay(150)
await delay("mid")
continue
}
@@ -420,7 +421,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
if (lastTextInput) {
;(lastTextInput as HTMLElement).click()
lastTextInput.focus()
await delay(50)
await delay("low")
lastTextInput.blur()
} else {
if (document.activeElement instanceof HTMLElement) {
@@ -431,7 +432,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
}
document.body.click()
}
await delay(150)
await delay("mid")
if (!f.isPicker && f.inputElement) lastTextInput = f.inputElement
}
@@ -1322,7 +1323,7 @@ async function b2FillDateTimeField(
// 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker
inputEl.focus()
;(inputEl as HTMLElement).click()
await delay(150)
await delay("mid")
const ok = await fillDatePicker(field)
if (ok) {
@@ -1388,21 +1389,21 @@ async function b2FillBeisenRadioGroup(
const childEl = item.firstElementChild as HTMLElement | null
if (childEl) {
childEl.click()
await delay(50)
await delay("low")
// 子子级也点一下确保触发
const grandChildEl = childEl.firstElementChild as HTMLElement | null
if (grandChildEl) {
grandChildEl.click()
await delay(50)
await delay("low")
}
} else {
// fallback:直接点击 radioItem 自身
;(item as HTMLElement).click()
await delay(50)
await delay("low")
}
// 4. 验证选中结果:检查子级是否出现 phoenix-radio--checked 类名
await delay(100)
await delay("mid")
const checkedEl = item.querySelector(".phoenix-radio--checked")
if (checkedEl) {
// 选中成功,radio group 容器变 greenTwo
@@ -1499,7 +1500,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
}
await delay(100)
await delay("mid")
}
}
} else {
@@ -1549,7 +1550,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
}).length
const clicked = await clickAddButton(addBtn, titleEl, nextTitleEl, beforeCount)
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)
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)
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 as HTMLElement).click()
inputEl.focus()
await delay(400)
await delay("high")
// 2. 在页面中找 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
if (clearBtn) {
clearBtn.click()
await delay(200) // 等待清空操作生效
await delay("mid") // 等待清空操作生效
console.log(`OfferPie: [北森级联] "${labelText}" 已清空已选内容`)
}
@@ -1704,7 +1705,7 @@ export async function fillBeisenCascadePicker(
forceSetValue(searchInput, fillValue)
searchInput.dispatchEvent(new Event("input", { bubbles: true }))
searchInput.dispatchEvent(new Event("change", { bubbles: true }))
await delay(300) // 等待搜索结果刷新(太短会导致选项列表未更新,选到错误选项)
await delay("mid") // 等待搜索结果刷新(太短会导致选项列表未更新,选到错误选项)
// 6. 检测选项集合位置(两种情况)
// 在 left-container 下查找
@@ -1728,7 +1729,7 @@ export async function fillBeisenCascadePicker(
const svgEl = iconSpan.querySelector("svg") as unknown as HTMLElement | null
if (svgEl) svgEl.dispatchEvent(new MouseEvent("click", { bubbles: true }))
optionClicked = true
await delay(200)
await delay("mid")
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
if (svgEl) svgEl.dispatchEvent(new MouseEvent("click", { bubbles: true }))
optionClicked = true
await delay(200)
await delay("mid")
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}" 已点击确定按钮`)
} else {
console.log(`OfferPie: [北森级联] "${labelText}" 未找到确定按钮`)
+13 -12
View File
@@ -7,7 +7,8 @@
* 不要在此文件中自行编写选择器操作逻辑,必须引用 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 { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
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 uploaded = await detectAndUploadResume(resumeUrl)
// console.log(`===== OfferPie: 简历上传 ${uploaded ? "成功" : "跳过(未找到上传按钮或失败)"} =====`)
// if (uploaded) await delay(1000) // 等待网站解析简历
// if (uploaded) await delay("high") // 等待网站解析简历
// 4.5 定位经历区块并统计已展开段数
const sectionResults = locateExperienceSections(document.body, lang)
@@ -331,7 +332,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
const ok = await fillSearchPickerField(f)
if (ok) { result.success++ } else { result.failed++ }
phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx })
await delay(100)
await delay("mid")
continue
} else {
await detectPickerField(f, lang)
@@ -346,7 +347,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
if (lastTextInput && !lastTextInputIsPicker) {
;(lastTextInput as HTMLElement).click()
lastTextInput.focus()
await delay(50)
await delay("low")
lastTextInput.blur()
} else {
if (document.activeElement instanceof HTMLElement) {
@@ -357,7 +358,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
}
document.body.click()
}
await delay(150)
await delay("mid")
if (!f.isPicker && f.inputElement && !isTimePeriodField(f.key) && !isTimeSingleField(f.key)) {
lastTextInput = f.inputElement
@@ -404,7 +405,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
if (lastTextInput) {
;(lastTextInput as HTMLElement).click()
lastTextInput.focus()
await delay(50)
await delay("low")
lastTextInput.blur()
} else {
if (document.activeElement instanceof HTMLElement) {
@@ -415,7 +416,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
}
document.body.click()
}
await delay(150)
await delay("mid")
if (!f.isPicker && f.inputElement) lastTextInput = f.inputElement
}
@@ -1257,7 +1258,7 @@ async function b2FillDateTimeField(
// 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker
inputEl.focus()
;(inputEl as HTMLElement).click()
await delay(150)
await delay("mid")
const ok = await fillDatePicker(field)
if (ok) {
@@ -1339,7 +1340,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
}
await delay(100)
await delay("mid")
}
}
} else {
@@ -1389,7 +1390,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
}).length
const clicked = await clickAddButton(addBtn, titleEl, nextTitleEl, beforeCount)
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)
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)
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 { fillDatePicker } from "./datePicker"
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
@@ -60,12 +56,12 @@ export function forceSetValue(el: HTMLInputElement | HTMLTextAreaElement, value:
/** 关闭弹出层 */
export async function closePopup(inputEl: HTMLElement) {
inputEl.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: 27, bubbles: true }))
await delay(100)
await delay("mid")
inputEl.blur()
inputEl.dispatchEvent(new Event("blur", { bubbles: true }))
await delay(100)
await delay("mid")
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 (isPicker) return await fillPickerField(field)
inputElement.focus()
await delay(50)
await delay("low")
forceSetValue(inputElement, fillValue)
console.log(`OfferPie: ✅ [输入框] 已填写 "${labelText}" = "${fillValue}"`)
return true
@@ -167,9 +163,9 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
// 搜不到时再补充搜索页面上其他可见弹出层(处理嵌套容器),最终都搜不到才关闭后走步骤1-4。
if (!isDateValue && field.pickerDropdownElement) {
const existingPopupEl = field.pickerDropdownElement
await delay(100) // 等待弹出层渲染完成
await delay("mid") // 等待弹出层渲染完成
if (clickBestOptionInDropdown(existingPopupEl, fillValue)) {
await delay(300); await closePopup(inputElement)
await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}" (已有弹出层)`)
return true
}
@@ -181,7 +177,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
const htmlEl = el as HTMLElement
if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0 && htmlEl !== existingPopupEl) {
if (clickBestOptionInDropdown(htmlEl, fillValue)) {
await delay(300); await closePopup(inputElement)
await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}" (嵌套弹出层)`)
return true
}
@@ -189,7 +185,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
}
// 仍然没找到,关闭弹出层后走步骤1-4 重新展开
await closePopup(inputElement)
await delay(200)
await delay("mid")
}
// ---- 步骤1:记录点击前的 DOM 快照 ----
@@ -218,7 +214,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
clickTargets[i].click()
}
inputElement.focus()
await delay(300)
await delay("mid")
if (isDateValue) return await fillDatePicker(field)
@@ -266,7 +262,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
if (dropdownEl) {
console.log(`OfferPie: [fillPicker] "${labelText}" 找到弹出层: ${dropdownEl.tagName}.${(dropdownEl.className || "").toString().split(" ")[0]}`)
if (clickBestOptionInDropdown(dropdownEl, fillValue)) {
await delay(300); await closePopup(inputElement)
await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true
}
@@ -274,23 +270,23 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
// ---- 步骤5fallback - 在弹出层中搜索(有弹出层在弹出层内搜,没有则全页面搜) ----
if (findAndClickOptionInVisiblePopups(fillValue, labelText, dropdownEl)) {
await delay(300); await closePopup(inputElement)
await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true
}
// ---- 步骤6:写入值触发搜索,再尝试一次 ----
forceSetValue(inputElement, fillValue)
await delay(500)
await delay("high")
if (findAndClickOptionInVisiblePopups(fillValue, labelText, dropdownEl)) {
await delay(300); await closePopup(inputElement)
await delay("mid"); await closePopup(inputElement)
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true
}
// ---- 步骤7:兜底 Enter 确认 ----
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
await delay(300)
await delay("mid")
await closePopup(inputElement)
console.log(`OfferPie: ⚠️ [选择器] 未找到匹配选项,已兜底填写 "${labelText}" = "${fillValue}"`)
return true
@@ -305,7 +301,7 @@ export async function fillAllFields(fields: MatchedFormField[]): Promise<{ total
if (field.inputType !== "radio" && !field.inputElement) { result.skipped++; continue }
const ok = await fillMatchedField(field)
if (ok) result.success++; else result.failed++
await delay(200)
await delay("mid")
}
console.log(`===== OfferPie: 自动填写完毕 总计 ${result.total} | 成功 ${result.success} | 失败 ${result.failed} | 跳过 ${result.skipped} =====`)
return result
@@ -353,7 +349,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
// 2. 点击 input 获取焦点并输入值
inputElement.focus()
;(inputElement as HTMLElement).click()
await delay(200)
await delay("mid")
forceSetValue(inputElement, fillValue)
// 3. 等待弹出层出现(异步下拉字段轮询等待,普通字段固定等待)
@@ -363,7 +359,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
// 接口异步下拉:轮询等待弹出层出现,最多等3秒
console.log(`OfferPie: [搜索选择器] "${labelText}" 异步下拉字段,等待接口返回...`)
for (let i = 0; i < 10; i++) {
await delay(200)
await delay("mid")
// 检查是否有新弹出层
for (const child of Array.from(document.body.children)) {
if (!beforeBodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) {
@@ -385,7 +381,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
if (hasNewPopup) break
}
} else {
await delay(401)
await delay("high")
// 检查 body 下新增的子元素
for (const child of Array.from(document.body.children)) {
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}"`)
inputElement.dispatchEvent(new Event("blur", { bubbles: true }))
// 调试:blur 后再验证
await delay(50)
await delay("low")
console.log(`OfferPie: [搜索选择器→普通输入] "${labelText}" blur后验证: value="${inputElement.value}"`)
return true
}
// 5. 有弹出层 → 走搜索选择器逻辑,等待搜索结果
await delay(500)
await delay("high")
// 【注意】对异步下拉字段,必须优先在检测到的弹出层元素内直接做文字匹配
// 不能走 tryClickDropdownListItem 全页面搜——异步下拉可能只有1-2个选项,全页面搜会被其他同规格元素干扰
if (isAsyncDropdown && newPopupEl) {
if (clickBestOptionInDropdown(newPopupEl, fillValue)) {
await delay(300)
await delay("mid")
await closePopup(inputElement)
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true
@@ -436,16 +432,16 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
// 在弹出层中查找匹配选项
const { findAndClickOptionInVisiblePopups } = await import("./pickerFill")
if (findAndClickOptionInVisiblePopups(fillValue, labelText, newPopupEl)) {
await delay(300)
await delay("mid")
await closePopup(inputElement)
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"`)
return true
}
// 6. 再等一会重试(有些网站接口慢)
await delay(600)
await delay("high")
if (findAndClickOptionInVisiblePopups(fillValue, labelText, newPopupEl)) {
await delay(300)
await delay("mid")
await closePopup(inputElement)
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"(重试)`)
return true
@@ -522,7 +518,7 @@ async function fillYearMonthPicker(inputEl: HTMLInputElement | HTMLTextAreaEleme
// 点击 input 展开下拉
inputEl.focus()
;(inputEl as HTMLElement).click()
await delay(300)
await delay("mid")
// 检测新出现的弹出层
let dropdownEl: HTMLElement | null = null
@@ -551,14 +547,14 @@ async function fillYearMonthPicker(inputEl: HTMLInputElement | HTMLTextAreaEleme
if (dropdownEl) {
// 在弹出层中精确匹配选项并点击
if (clickBestOptionInDropdown(dropdownEl, value)) {
await delay(200)
await delay("mid")
return true
}
}
// 兜底:直接写入值
forceSetValue(inputEl, value)
await delay(100)
await delay("mid")
return true
}
@@ -630,10 +626,10 @@ export async function fillTimePeriodField(
usedInputs.add(yearMonthInputs[3])
}
await delay(200)
await delay("mid")
// 点击空白关闭弹窗
document.body.click()
await delay(100)
await delay("mid")
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${start.year}.${start.month} - ${end.year}.${end.month}`)
return true
@@ -649,7 +645,7 @@ export async function fillTimePeriodField(
usedInputs.add(yearMonthInputs[1])
}
document.body.click()
await delay(100)
await delay("mid")
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${start.year}.${start.month}`)
return true
}
@@ -671,14 +667,14 @@ export async function fillTimePeriodField(
}
startInput.focus()
;(startInput as HTMLElement).click()
await delay(300)
await delay("mid")
const startOk = await fillDatePicker(startField)
if (startOk) {
usedInputs.add(startInput)
console.log(`OfferPie: [时间选择器] "${field.labelText}" 开始时间填写成功: ${startDate}`)
}
await delay(500)
await delay("high")
// 从开始时间 input 往后找最近的下一个 input 作为结束时间
if (endDate) {
@@ -703,7 +699,7 @@ export async function fillTimePeriodField(
}
endInput.focus()
;(endInput as HTMLElement).click()
await delay(300)
await delay("mid")
const endOk = await fillDatePicker(endField)
if (endOk) {
usedInputs.add(endInput)
@@ -768,7 +764,7 @@ export async function fillTimeSingleField(
usedInputs.add(yearMonthInputs[1])
}
document.body.click()
await delay(100)
await delay("mid")
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${year}.${month}`)
return true
}
+27 -26
View File
@@ -14,7 +14,8 @@
*/
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()
await delay(150)
await delay("mid")
const afterYear = readCurrentYear(panel)
const afterMonth = readCurrentMonth(panel)
@@ -626,21 +627,21 @@ async function detectNavButtons(panel: CalendarPanelInfo): Promise<NavButtons> {
if (restoreBtn) {
restoreBtn.click()
await delay(150)
await delay("mid")
} else {
// 没有已知反向按钮,尝试用位置对称的按钮恢复
// 如果当前按钮在前半部分,对称按钮在后半部分
const symmetricIdx = buttons.length - 1 - i
if (symmetricIdx !== i && symmetricIdx >= 0 && symmetricIdx < buttons.length) {
buttons[symmetricIdx].click()
await delay(150)
await delay("mid")
// 验证是否恢复
const checkYear = readCurrentYear(panel)
const checkMonth = readCurrentMonth(panel)
if (checkYear !== beforeYear || checkMonth !== beforeMonth) {
// 没恢复成功,再点一次原按钮的对称位置
buttons[symmetricIdx].click()
await delay(150)
await delay("mid")
}
}
}
@@ -684,7 +685,7 @@ async function restoreToDate(
if (yearBtn) {
for (let i = 0; i < Math.abs(yearDiff) && i < 50; i++) {
yearBtn.click()
await delay(80)
await delay("low")
}
}
}
@@ -700,7 +701,7 @@ async function restoreToDate(
if (monthBtn) {
for (let i = 0; i < Math.abs(monthDiff) && i < 24; i++) {
monthBtn.click()
await delay(80)
await delay("low")
}
}
}
@@ -731,7 +732,7 @@ async function navigateToYearMonth(
console.log(`OfferPie: [datePicker] 年份需要${yearDiff > 0 ? "增加" : "减少"} ${steps}`)
for (let i = 0; i < steps && i < 100; i++) {
yearBtn.click()
await delay(100)
await delay("mid")
// 验证年份确实变了
const newYear = readCurrentYear(panel)
if (newYear === targetYear) break
@@ -744,7 +745,7 @@ async function navigateToYearMonth(
console.log(`OfferPie: [datePicker] 无年份按钮,用月份按钮跨 ${Math.abs(totalMonthDiff)} 个月`)
for (let i = 0; i < Math.abs(totalMonthDiff) && i < 200; i++) {
monthBtn.click()
await delay(60)
await delay("low")
}
}
}
@@ -770,7 +771,7 @@ async function navigateToYearMonth(
console.log(`OfferPie: [datePicker] 月份需要${monthDiff > 0 ? "增加" : "减少"} ${steps}`)
for (let i = 0; i < steps && i < 24; i++) {
btn.click()
await delay(100)
await delay("mid")
const newMonth = readCurrentMonth(panel)
if (newMonth === targetMonth) break
}
@@ -887,7 +888,7 @@ async function tryFillMonthPanel(
console.log(`OfferPie: [datePicker] 月份面板年份导航: ${yearDiff > 0 ? "+" : ""}${yearDiff}`)
for (let i = 0; i < steps && i < 50; i++) {
yearBtn.click()
await delay(150)
await delay("mid")
const newYear = readCurrentYear(tempPanel)
if (newYear === targetYear) break
}
@@ -901,7 +902,7 @@ async function tryFillMonthPanel(
const steps = Math.abs(yearDiff)
for (let i = 0; i < steps && i < 50; i++) {
btn.click()
await delay(150)
await delay("mid")
const newYear = readCurrentYear(tempPanel)
if (newYear === targetYear) break
}
@@ -926,9 +927,9 @@ async function tryFillMonthPanel(
if (inner && isVisible(inner)) deepest = inner as HTMLElement
deepest.click()
await delay(100)
await delay("mid")
if (deepest !== cell) cell.click()
await delay(300)
await delay("mid")
await closePopup(inputElement)
console.log(`OfferPie: ✅ [月份选择器] 已选择 "${labelText}" = "${fillValue}" (${targetYear}${targetMonth}月)`)
return true
@@ -1059,12 +1060,12 @@ async function clickDayCell(
// 三层点击:叶子 → 父级 → 原始元素
deepest.click()
await delay(50)
await delay("low")
if (deepest.parentElement) deepest.parentElement.click()
await delay(50)
await delay("low")
if (cell !== deepest && cell !== deepest.parentElement) (cell as HTMLElement).click()
await delay(300)
await delay("mid")
await closePopup(inputElement)
console.log(`OfferPie: ✅ [日期] 已选择 "${labelText}" = "${fillValue}" (日期 ${targetDay})`)
return true
@@ -1158,9 +1159,9 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
console.warn(`OfferPie: [datePicker] 无法解析日期值: "${fillValue}"`)
// 兜底:直接写入值 + Enter
forceSetValue(inputElement, fillValue)
await delay(300)
await delay("mid")
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
await delay(300)
await delay("mid")
await closePopup(inputElement as HTMLElement)
return true
}
@@ -1170,7 +1171,7 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
// ---- 步骤2:写入日期字符串(触发面板联动) ----
forceSetValue(inputElement, fillValue)
await delay(500)
await delay("high")
// ---- 步骤2.5:如果只有年月(day=0),检测是否为月份面板 ----
if (targetDay === 0) {
@@ -1208,13 +1209,13 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
if (targetDay === 0) {
// 只需要年月,不需要点日期格子。尝试点击月份面板中的月份
// 如果导航成功了,可能面板已经关闭(某些组件选完月份自动关闭)
await delay(200)
await delay("mid")
await closePopup(inputElement as HTMLElement)
console.log(`OfferPie: ✅ [日期] 已导航到 "${labelText}" = "${fillValue}" (${targetYear}${targetMonth}月)`)
return true
}
await delay(200)
await delay("mid")
const clicked = await clickDayCell(panel, targetDay, labelText, fillValue, inputElement as HTMLElement)
if (clicked) return true
@@ -1250,10 +1251,10 @@ async function fallbackClickDay(
break
}
}
deepest.click(); await delay(50)
if (deepest.parentElement) deepest.parentElement.click(); await delay(50)
deepest.click(); await delay("low")
if (deepest.parentElement) deepest.parentElement.click(); await delay("low")
if (cell !== deepest && cell !== deepest.parentElement) (cell as HTMLElement).click()
await delay(300)
await delay("mid")
await closePopup(inputElement)
console.log(`OfferPie: ✅ [日期-兜底] 已选择 "${labelText}" = "${fillValue}" (日期 ${targetDay})`)
return true
@@ -1263,7 +1264,7 @@ async function fallbackClickDay(
// 最终兜底:Enter 确认
console.warn(`OfferPie: ⚠ [日期] "${labelText}" 未找到日历格子,Enter 确认`)
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
await delay(300)
await delay("mid")
await closePopup(inputElement)
return true
}
+3 -3
View File
@@ -15,7 +15,7 @@
*/
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 type { ExperienceSection, ExperienceSectionConfig, ResumeData } from "./types"
@@ -766,7 +766,7 @@ export async function clickAddButton(
// 逐层点击,每层点击后检测 input 数量是否增加
for (const target of clickTargets) {
target.click()
await delay(400)
await delay("high")
const afterInputs = findInputsInRange(titleEl, nextTitleEl)
if (afterInputs.length > beforeInputCount) {
@@ -1105,7 +1105,7 @@ export async function expandExperienceSections(
// 点击添加按钮
const clickSuccess = await clickAddButton(addBtn, result.titleElement, nextTitleEl, beforeInputCount)
await delay(300)
await delay("mid")
if (clickSuccess) {
// 通过 DOM 差异找出新增的顶层容器(即一段经历的完整 DOM)
+3 -3
View File
@@ -17,7 +17,7 @@
import type { MatchedFormField, UILibPickerConfig } from "./types"
import { UI_LIB_PICKER_CONFIGS } from "./constants"
import { delay } from "./autofill"
import { delay } from "~utils/delay"
/** 通过提示文字判断是否为选择器 */
function hasPickerHintText(field: MatchedFormField, lang: "zh" | "en"): boolean {
@@ -109,7 +109,7 @@ async function dismissPopup() {
document.activeElement.blur()
}
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) {
const before = snapshotDOM()
target.click()
await delay(200)
await delay("mid")
if (hasNewPopup(before)) {
console.log(`OfferPie: [${field.key}] 点击 ${target.tagName}.${(target.className || "").toString().split(" ")[0]} 后检测到弹出层`)
+2 -2
View File
@@ -3,7 +3,7 @@
* 负责:检测页面上的简历上传按钮,从 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) {
target.click()
await delay(300)
await delay("mid")
for (const input of Array.from(document.querySelectorAll('input[type="file"]'))) {
const id = input.id?.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]))
}