增加其他3种特殊平台模式就,处理下表单下拉组件输入框不绑定选项值识别故障问题

This commit is contained in:
2026-08-04 15:49:45 +08:00
parent bc8ef50bd1
commit c9fb0c5644
9 changed files with 5218 additions and 10 deletions
+119 -7
View File
@@ -11,10 +11,14 @@ import { getCustomizeResume } from "~api/aiApi"
import { getMemberStatus } from "~api/dataApi"
import { handleAutoFillCommon } from "~handlers/handleAutoFillCommon"
import { handleAutoFillBeisen } from "~handlers/handleAutoFillBeisen"
import { scanPageFields, extractNonResumeFields, printFieldStats } from "~lib/fillStats"
import { handleAutoFillMoka } from "~handlers/handleAutoFillMoka"
import { handleAutoFillFeishu } from "~handlers/handleAutoFillFeishu"
import { handleAutoFillHotjob } from "~handlers/handleAutoFillHotjob"
import { scanPageFields, extractNonResumeFields, printFieldStats, collectPickerOptionTexts, getPickerDisplayValue } from "~lib/fillStats"
import type { TitleStat } from "~lib/fillStats"
import type { MatchedFormField, ResumeData, JobInfo } from "~lib/types"
import { createChannelBridge } from "~lib/channelBridge"
import { buildResumeExcludeTexts } from "~lib/resumeDataHelper"
import logoImg from "data-base64:~/../assets/logo-offerpai.png"
import { config as appConfig } from "~config"
import "./SidebarPanel.scss"
@@ -96,6 +100,9 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
isDragging: false, startX: 0, startY: 0, startOffsetX: 0, startOffsetY: 0, longPressTimer: null, isLongPress: false
})
/** 选择器选项文字缓存(collectPickerOptionTexts 收集后存入,handleSaveUserInput 用于排除) */
const pickerOptionTextsRef = useRef<Set<string>>(new Set())
/** 停止当前循环扫描 */
const stopScanLoop = () => {
if (scanIntervalRef.current) {
@@ -128,17 +135,67 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
}
/** 开启循环扫描(每秒执行一次) */
const startScanLoop = (params: { siteMode?: "beisen"; sectionResults?: any[]; expandedResults?: any[]; excludeTexts?: Set<string> }) => {
const startScanLoop = (params: { siteMode?: "beisen" | "moka" | "feishu" | "hotjob"; sectionResults?: any[]; expandedResults?: any[]; excludeTexts?: Set<string> }) => {
stopScanLoop()
const hasFullData = !!(params.sectionResults && params.expandedResults)
/**
* 同步构建排除集合:合并 params.excludeTexts + 当前 resumeData 中的所有值
* 避免将已填入表单的简历值(如"汉族"、"广州"等)误认为表单标签
*/
const buildExcludeTexts = (): Set<string> => {
const baseSet = buildResumeExcludeTexts(resumeData)
// 合并外部传入的静态排除集合(第二次调用时由 fillResult.resumeData 构建)
if (params.excludeTexts) {
for (const t of params.excludeTexts) baseSet.add(t)
}
return baseSet
}
/**
* 补充选择器展示值:遍历已识别字段,对 value 为空的字段调用 getPickerDisplayValue
* 从 DOM 中提取展示文字与已收集的弹出层选项比对,匹配成功则更新字段 value
*/
const fillPickerDisplayValues = (fieldStats: TitleStat[]) => {
const options = pickerOptionTextsRef.current
if (options.size === 0) return
// 北森模式(zhiye.com)的选择器值直接写在输入框里,不需要额外提取展示值
if (window.location.hostname.includes("zhiye.com")) return
for (const ts of fieldStats) {
for (let fIdx = 0; fIdx < ts.fields.length; fIdx++) {
const f = ts.fields[fIdx]
// 已有值的跳过
if (f.value) continue
if (!f.inputElement) continue
const inputEl = f.inputElement as HTMLElement
// input.value 有值的跳过
if ((inputEl as HTMLInputElement).value?.trim()) continue
// 找同大标题内下一个字段的标签文字作为边界
const nextField = ts.fields[fIdx + 1]
const nextLabelText = nextField?.labelText || null
// 调用 getPickerDisplayValue 比对
const displayVal = getPickerDisplayValue(inputEl, f.labelText, nextLabelText, options)
if (displayVal) {
f.value = displayVal
f.filled = true
f.color = "green"
}
}
}
}
// 立即执行一次
const stats = scanPageFields(params)
const stats = scanPageFields({ ...params, excludeTexts: buildExcludeTexts() })
// 补充选择器展示值:对 value 为空的字段,从 DOM 中提取展示文字与选项比对
fillPickerDisplayValues(stats)
setFieldStats(stats)
findUpdatedField(stats) // 初始化快照,不滚动
if (hasFullData) handleSaveUserInput()
// 每秒循环
scanIntervalRef.current = setInterval(() => {
const s = scanPageFields(params)
const s = scanPageFields({ ...params, excludeTexts: buildExcludeTexts() })
// 补充选择器展示值
fillPickerDisplayValues(s)
setFieldStats(s)
if (ENABLE_AUTO_SCROLL_TO_UPDATED) {
const updatedEl = findUpdatedField(s)
@@ -440,14 +497,27 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
// 检测当前页面域名,判断走哪个处理模式
const currentHost = window.location.hostname
const isBeisen = currentHost.includes("zhiye.com") // 北森招聘平台域名特征
const scanSiteMode = isBeisen ? "beisen" as const : undefined
const isMoka = currentHost.includes("mokahr.com") // 摩卡招聘平台域名特征
const isFeishu = currentHost.includes("feishu.cn") // 飞书招聘平台域名特征
const isHotjob = currentHost.includes("hotjob.cn") // Hotjob招聘平台域名特征
const scanSiteMode = isBeisen ? "beisen" as const
: isMoka ? "moka" as const
: isFeishu ? "feishu" as const
: isHotjob ? "hotjob" as const
: undefined
// 第一次开启循环扫描(不传 sectionResults,阶段A之前就开始展示)
startScanLoop({ siteMode: scanSiteMode })
const fillResult = isBeisen
? await handleAutoFillBeisen({ resumeData, jobInfo })
: await handleAutoFillCommon({ resumeData, jobInfo })
: isMoka
? await handleAutoFillMoka({ resumeData, jobInfo })
: isFeishu
? await handleAutoFillFeishu({ resumeData, jobInfo })
: isHotjob
? await handleAutoFillHotjob({ resumeData, jobInfo })
: await handleAutoFillCommon({ resumeData, jobInfo })
// 将处理器返回的结果同步到组件状态
setPageLang(fillResult.lang) // 页面语言(影响后续标签匹配用中文还是英文)
@@ -481,6 +551,22 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
}
}
}
// 收集页面上所有选择器字段的下拉选项文字,合并到排除集合
// 避免选择器已选值(如"维吾尔族")被误认为表单标签
// 北森模式(zhiye.com)选择器值直接写在输入框里,跳过整套弹出层收集逻辑
if (!isBeisen) {
const preFieldStats = scanPageFields({
siteMode: scanSiteMode,
sectionResults: fillResult.sectionResults,
expandedResults: fillResult.expandedResults,
excludeTexts: scanExcludeTexts,
})
const pickerOptionTexts = await collectPickerOptionTexts(preFieldStats)
for (const t of pickerOptionTexts) scanExcludeTexts.add(t)
// 存入 ref,供 handleSaveUserInput 使用
pickerOptionTextsRef.current = pickerOptionTexts
}
startScanLoop({
siteMode: scanSiteMode,
sectionResults: fillResult.sectionResults,
@@ -508,7 +594,11 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
// 检测当前网站模式
const currentHost = window.location.hostname
const siteMode = currentHost.includes("zhiye.com") ? "beisen" as const : undefined
const siteMode = currentHost.includes("zhiye.com") ? "beisen" as const
: currentHost.includes("mokahr.com") ? "moka" as const
: currentHost.includes("feishu.cn") ? "feishu" as const
: currentHost.includes("hotjob.cn") ? "hotjob" as const
: undefined
// 从简历数据和缓存数据中提取所有已填值,构建排除集合
// 避免 findLabelForInput 把这些值文字误认为是表单字段标签
@@ -550,10 +640,32 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
}
}
}
// 合并选择器选项文字(collectPickerOptionTexts 收集的下拉选项)
for (const t of pickerOptionTextsRef.current) excludeTexts.add(t)
// 调用 fillStats 扫描页面所有字段
const titleStats = scanPageFields({ siteMode, excludeTexts })
// 补充选择器展示值(与 startScanLoop 逻辑一致)
const pickerOpts = pickerOptionTextsRef.current
if (pickerOpts.size > 0) {
for (const ts of titleStats) {
for (let fIdx = 0; fIdx < ts.fields.length; fIdx++) {
const f = ts.fields[fIdx]
if (f.value) continue
if (!f.inputElement) continue
if ((f.inputElement as HTMLInputElement).value?.trim()) continue
const nextField = ts.fields[fIdx + 1]
const nextLabelText = nextField?.labelText || null
const displayVal = getPickerDisplayValue(f.inputElement as HTMLElement, f.labelText, nextLabelText, pickerOpts)
if (displayVal) {
f.value = displayVal
f.filled = true
}
}
}
}
// 提取非简历格式字段(B2阶段字段)
const nonResumeData = extractNonResumeFields(titleStats)