mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-05-06 20:02:47 +08:00
1773 lines
75 KiB
JavaScript
1773 lines
75 KiB
JavaScript
import {
|
||
sleep,
|
||
sleepWithRandomDelay
|
||
} from '@geekgeekrun/utils/sleep.mjs'
|
||
|
||
import fs from 'node:fs'
|
||
import os from 'node:os'
|
||
import { get__dirname } from '@geekgeekrun/utils/legacy-path.mjs';
|
||
import path from 'node:path';
|
||
import JSON5 from 'json5'
|
||
import { EventEmitter } from 'node:events'
|
||
import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
|
||
|
||
import { readConfigFile, writeStorageFile, ensureConfigFileExist, readStorageFile, ensureStorageFileExist } from './runtime-file-utils.mjs'
|
||
import {
|
||
calculateTotalCombinations,
|
||
combineFiltersWithConstraintsGenerator,
|
||
checkAnyCombineBossRecommendFilterHasCondition,
|
||
formatStaticCombineFilters,
|
||
} from './combineCalculator.mjs'
|
||
import { default as jobFilterConditions } from './internal-config/job-filter-conditions-20241002.json'
|
||
import { default as rawIndustryFilterExemption } from './internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||
import { ChatStartupFrom } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatStartupLog'
|
||
import {
|
||
MarkAsNotSuitReason,
|
||
MarkAsNotSuitOp,
|
||
StrategyScopeOptionWhenMarkJobNotMatch,
|
||
SalaryCalculateWay,
|
||
JobDetailRegExpMatchLogic,
|
||
JobSource,
|
||
CombineRecommendJobFilterType
|
||
} from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||
import {
|
||
activeDescList,
|
||
RECOMMEND_JOB_ENTRY_SELECTOR,
|
||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
|
||
SEARCH_BOX_SELECTOR,
|
||
} from './constant.mjs'
|
||
import { parseSalary } from "@geekgeekrun/sqlite-plugin/dist/utils/parser"
|
||
import { waitForSageTimeOrJustContinue } from './sage-time.mjs'
|
||
import cityGroupData from './cityGroup.mjs'
|
||
import { hasIntersection } from '@geekgeekrun/utils/number.mjs';
|
||
const flattedCityList = []
|
||
;(cityGroupData?.zpData?.cityGroup ?? []).forEach(it => {
|
||
const firstChar = it.firstChar
|
||
it.cityList.forEach(city => {
|
||
flattedCityList.push({
|
||
...city,
|
||
firstChar
|
||
})
|
||
})
|
||
})
|
||
|
||
const jobFilterConditionsMapByCode = {}
|
||
Object.values(jobFilterConditions).forEach(arr => {
|
||
arr.forEach(option => {
|
||
jobFilterConditionsMapByCode[option.code] = option
|
||
})
|
||
})
|
||
|
||
let industryFilterCursorIndex = 0;
|
||
const industryFilterExemption = JSON.parse(JSON.stringify(rawIndustryFilterExemption))
|
||
const industryFilterConditionsMapByIndex = {}
|
||
const industryFilterConditionsMapByCode = {}
|
||
const industryFilterConditionCodeToIndexMap = {}
|
||
industryFilterExemption.forEach(item => {
|
||
if (!Array.isArray(item.subLevelModelList)) {
|
||
return
|
||
}
|
||
item.subLevelModelList.forEach(option => {
|
||
industryFilterConditionsMapByCode[option.code] = option
|
||
industryFilterConditionsMapByIndex[industryFilterCursorIndex] = option
|
||
industryFilterConditionCodeToIndexMap[option.code] = industryFilterCursorIndex
|
||
industryFilterCursorIndex++
|
||
})
|
||
})
|
||
|
||
ensureConfigFileExist()
|
||
ensureStorageFileExist()
|
||
|
||
const isUiDev = process.env.NODE_ENV === 'development'
|
||
export const autoStartChatEventBus = new EventEmitter()
|
||
|
||
/**
|
||
* @type { import("puppeteer") }
|
||
*/
|
||
let puppeteer
|
||
let StealthPlugin
|
||
let LaodengPlugin
|
||
let AnonymizeUaPlugin
|
||
export async function initPuppeteer () {
|
||
// production
|
||
const importResult = await Promise.all(
|
||
[
|
||
import('puppeteer-extra'),
|
||
import('puppeteer-extra-plugin-stealth'),
|
||
import('@geekgeekrun/puppeteer-extra-plugin-laodeng'),
|
||
import('puppeteer-extra-plugin-anonymize-ua')
|
||
]
|
||
)
|
||
puppeteer = importResult[0].default
|
||
StealthPlugin = importResult[1].default
|
||
LaodengPlugin = importResult[2].default
|
||
AnonymizeUaPlugin = importResult[3].default
|
||
puppeteer.use(StealthPlugin())
|
||
puppeteer.use(LaodengPlugin())
|
||
puppeteer.use(AnonymizeUaPlugin({ makeWindows: false }))
|
||
return {
|
||
puppeteer,
|
||
StealthPlugin,
|
||
LaodengPlugin,
|
||
AnonymizeUaPlugin
|
||
}
|
||
}
|
||
|
||
const commonJobConditionConfig = readConfigFile('common-job-condition-config.json')
|
||
const fieldsForUseCommonConfig = readConfigFile('boss.json').fieldsForUseCommonConfig ?? {}
|
||
|
||
const targetCompanyList = (
|
||
!fieldsForUseCommonConfig.expectCompanies ?
|
||
readConfigFile('target-company-list.json')
|
||
:
|
||
commonJobConditionConfig.expectCompanies
|
||
).filter(it => !!it.trim());
|
||
const combineRecommendJobFilterType = readConfigFile('boss.json').combineRecommendJobFilterType ?? CombineRecommendJobFilterType.ANY_COMBINE
|
||
|
||
const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter
|
||
const staticCombineRecommendJobFilterConditions = readConfigFile('boss.json').staticCombineRecommendJobFilterConditions ?? []
|
||
let isSkipEmptyConditionForCombineRecommendJobFilter = readConfigFile('boss.json').isSkipEmptyConditionForCombineRecommendJobFilter
|
||
if (!checkAnyCombineBossRecommendFilterHasCondition(anyCombineRecommendJobFilter)) {
|
||
isSkipEmptyConditionForCombineRecommendJobFilter = false
|
||
}
|
||
const expectJobRegExpStr = readConfigFile('boss.json').expectJobRegExpStr
|
||
const jobNotMatchStrategy = readConfigFile('boss.json').jobNotMatchStrategy ?? MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||
|
||
const expectCityNotMatchStrategy = readConfigFile('boss.json').expectCityNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
|
||
const expectCityList = (
|
||
!fieldsForUseCommonConfig.city ?
|
||
readConfigFile('boss.json').expectCityList
|
||
:
|
||
commonJobConditionConfig.expectCityList
|
||
) ?? []
|
||
|
||
const strategyScopeOptionWhenMarkJobCityNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkJobCityNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
|
||
|
||
// salary
|
||
const expectSalaryLow = parseFloat(
|
||
!fieldsForUseCommonConfig.salary ?
|
||
readConfigFile('boss.json').expectSalaryLow
|
||
:
|
||
commonJobConditionConfig.expectSalaryLow
|
||
) || null
|
||
const expectSalaryHigh = parseFloat(
|
||
!fieldsForUseCommonConfig.salary ?
|
||
readConfigFile('boss.json').expectSalaryHigh
|
||
:
|
||
commonJobConditionConfig.expectSalaryHigh
|
||
) || null
|
||
const expectSalaryCalculateWay = (
|
||
!fieldsForUseCommonConfig.salary ?
|
||
readConfigFile('boss.json').expectSalaryCalculateWay
|
||
:
|
||
commonJobConditionConfig.expectSalaryCalculateWay
|
||
) ?? SalaryCalculateWay.MONTH_SALARY
|
||
const expectSalaryNotMatchStrategy = readConfigFile('boss.json').expectSalaryNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
|
||
const isSalaryFilterEnabled = expectSalaryLow || expectSalaryHigh
|
||
const strategyScopeOptionWhenMarkSalaryNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkSalaryNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
|
||
|
||
// work exp
|
||
let expectWorkExpList = readConfigFile('boss.json').expectWorkExpList ?? []
|
||
const expectWorkExpListSet = new Set(expectWorkExpList)
|
||
if (
|
||
expectWorkExpListSet.has('应届生') ||
|
||
expectWorkExpListSet.has('在校生')
|
||
) {
|
||
expectWorkExpListSet.delete('应届生')
|
||
expectWorkExpListSet.delete('在校生')
|
||
expectWorkExpListSet.add('在校/应届')
|
||
}
|
||
expectWorkExpList = Array.from(expectWorkExpListSet)
|
||
|
||
const expectWorkExpNotMatchStrategy = readConfigFile('boss.json').expectWorkExpNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
|
||
const strategyScopeOptionWhenMarkJobWorkExpNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkJobWorkExpNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
|
||
|
||
let jobDetailRegExpMatchLogic = (
|
||
!fieldsForUseCommonConfig.jobDetail ?
|
||
readConfigFile('boss.json').jobDetailRegExpMatchLogic
|
||
:
|
||
commonJobConditionConfig.jobDetailRegExpMatchLogic
|
||
) ?? JobDetailRegExpMatchLogic.EVERY
|
||
|
||
const markAsNotActiveSelectedTimeRange = (() => {
|
||
let n = readConfigFile('boss.json').markAsNotActiveSelectedTimeRange
|
||
if (
|
||
typeof n !== 'number' || isNaN(parseInt(n)) || n >= activeDescList.length || n < 0
|
||
) {
|
||
n = 7
|
||
}
|
||
return n
|
||
})()
|
||
const jobNotActiveStrategy = (() => {
|
||
let value = readConfigFile('boss.json').jobNotActiveStrategy ?? MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||
if (markAsNotActiveSelectedTimeRange === 0) {
|
||
value = MarkAsNotSuitOp.NO_OP
|
||
}
|
||
return value
|
||
})()
|
||
|
||
let {
|
||
expectJobNameRegExpStr,
|
||
expectJobTypeRegExpStr,
|
||
expectJobDescRegExpStr,
|
||
} = !fieldsForUseCommonConfig.jobDetail ? readConfigFile('boss.json') : commonJobConditionConfig
|
||
if (
|
||
!fieldsForUseCommonConfig.jobDetail &&
|
||
expectJobRegExpStr &&
|
||
!expectJobNameRegExpStr &&
|
||
!expectJobTypeRegExpStr &&
|
||
!expectJobDescRegExpStr
|
||
) {
|
||
expectJobNameRegExpStr = expectJobRegExpStr
|
||
expectJobTypeRegExpStr = expectJobRegExpStr
|
||
expectJobDescRegExpStr = expectJobRegExpStr
|
||
}
|
||
|
||
if (
|
||
[
|
||
expectJobNameRegExpStr,
|
||
expectJobTypeRegExpStr,
|
||
expectJobDescRegExpStr,
|
||
].map(it => Boolean(it?.trim())).every(it => !it)
|
||
) {
|
||
jobDetailRegExpMatchLogic = JobDetailRegExpMatchLogic.EVERY
|
||
}
|
||
|
||
let {
|
||
jobSourceList
|
||
} = readConfigFile('boss.json')
|
||
const normalizedJobSource = []
|
||
const addedSourceSet = new Set()
|
||
for (const source of (jobSourceList ?? [])) {
|
||
if (addedSourceSet.has(source.type)) {
|
||
continue
|
||
}
|
||
if (!source?.enabled) {
|
||
continue
|
||
}
|
||
if (source.type === 'search') {
|
||
for (const searchOption of (source.children ?? [])) {
|
||
if (!searchOption.enabled || !searchOption.keyword?.trim()) {
|
||
continue
|
||
}
|
||
const key = [
|
||
source.type,
|
||
searchOption.keyword.trim()
|
||
].join('__')
|
||
if (addedSourceSet.has(key)) {
|
||
continue
|
||
}
|
||
normalizedJobSource.push({
|
||
type: 'search',
|
||
keyword: searchOption.keyword.trim()
|
||
})
|
||
addedSourceSet.add(key)
|
||
}
|
||
addedSourceSet.add(source.type)
|
||
}
|
||
else {
|
||
normalizedJobSource.push({
|
||
type: source.type,
|
||
})
|
||
addedSourceSet.add(source.type)
|
||
}
|
||
}
|
||
if (!normalizedJobSource?.length) {
|
||
normalizedJobSource.push({
|
||
type: 'expect'
|
||
})
|
||
}
|
||
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
|
||
const recommendJobPageUrl = `https://www.zhipin.com/web/geek/jobs`
|
||
|
||
const expectCompanySet = new Set(targetCompanyList)
|
||
const enableCompanyAllowList = Boolean(expectCompanySet.size)
|
||
const blockCompanyNameRegExpStr = (
|
||
!fieldsForUseCommonConfig.blockCompanyNameRegExpStr ?
|
||
readConfigFile('boss.json').blockCompanyNameRegExpStr
|
||
:
|
||
commonJobConditionConfig.blockCompanyNameRegExpStr
|
||
) ?? ''
|
||
const blockCompanyNameRegExp = (() => {
|
||
if (!blockCompanyNameRegExpStr?.trim()) {
|
||
return null
|
||
}
|
||
try {
|
||
return new RegExp(blockCompanyNameRegExpStr, 'im')
|
||
}
|
||
catch {
|
||
return null
|
||
}
|
||
})()
|
||
const blockCompanyNameRegMatchStrategy = readConfigFile('boss.json').blockCompanyNameRegMatchStrategy ?? MarkAsNotSuitOp.NO_OP
|
||
|
||
/**
|
||
* @type { import('puppeteer').Browser }
|
||
*/
|
||
let browser
|
||
/**
|
||
* @type { import('puppeteer').Page }
|
||
*/
|
||
let page
|
||
|
||
const blockBossNotNewChat = new Set()
|
||
const blockBossNotActive = new Set()
|
||
const blockJobNotSuit = new Set()
|
||
|
||
async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
||
/**
|
||
* @type {{chosenReasonInUi?: { code: number, text: string}}}
|
||
*/
|
||
const result = {}
|
||
const notSuitableFeedbackButtonProxy = await page.$('.job-detail-box .job-detail-operate .not-suitable')
|
||
if (notSuitableFeedbackButtonProxy) {
|
||
await notSuitableFeedbackButtonProxy.evaluate(el => {
|
||
el.scrollIntoView({
|
||
block: 'center'
|
||
})
|
||
})
|
||
await sleep(200)
|
||
await notSuitableFeedbackButtonProxy.click()
|
||
const rawReasonResData = (await (await page.waitForResponse(
|
||
response => {
|
||
if (
|
||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/reasons.json')
|
||
) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
)).json())?.zpData?.result ?? [];
|
||
const reasonCodeToTextMap = await readStorageFile('job-not-suit-reason-code-to-text-cache.json')
|
||
for(const it of rawReasonResData) {
|
||
reasonCodeToTextMap[it.code] = it.text?.content ?? ''
|
||
}
|
||
await writeStorageFile('job-not-suit-reason-code-to-text-cache.json', reasonCodeToTextMap)
|
||
await sleepWithRandomDelay(2000)
|
||
const chooseReasonDialogProxy = await(async() => {
|
||
const alls = await page.$$('.zp-dialog-wrap.zp-feedback-dialog')
|
||
return alls?.[alls.length - 1]
|
||
})()
|
||
let isOptionChosen = false
|
||
if (chooseReasonDialogProxy) {
|
||
switch (reasonCode) {
|
||
case MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT: {
|
||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title*="公司"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="薪资"]`))
|
||
if (opProxy) {
|
||
await opProxy.click()
|
||
isOptionChosen = true
|
||
}
|
||
break
|
||
}
|
||
case MarkAsNotSuitReason.BOSS_INACTIVE: {
|
||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title="BOSS活跃度低"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="职位停招/招满"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
|
||
if (opProxy) {
|
||
await opProxy.click()
|
||
isOptionChosen = true
|
||
}
|
||
break
|
||
}
|
||
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
|
||
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT: {
|
||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="城市"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="公司"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
|
||
if (opProxy) {
|
||
await opProxy.click()
|
||
isOptionChosen = true
|
||
}
|
||
break
|
||
}
|
||
case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: {
|
||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title*="薪资"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title$="城市"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="公司"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
|
||
if (opProxy) {
|
||
await opProxy.click()
|
||
isOptionChosen = true
|
||
}
|
||
break
|
||
}
|
||
case MarkAsNotSuitReason.JOB_NOT_SUIT:
|
||
default: {
|
||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="职位"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
|
||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
|
||
if (opProxy) {
|
||
await opProxy.click()
|
||
isOptionChosen = true
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
if (isOptionChosen) {
|
||
await sleepWithRandomDelay(1500)
|
||
const confirmButtonProxy = await chooseReasonDialogProxy.$(`.zp-dialog-footer .zp-btn.zp-btn-sure`)
|
||
await confirmButtonProxy.click()
|
||
const response = await page.waitForResponse(
|
||
response => {
|
||
if (
|
||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/save.json')
|
||
) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
)
|
||
/**
|
||
* scene=4&code=41&feedbackReason=&securityId=
|
||
*/
|
||
const requestBody = response.request().postData()
|
||
const chosenCode = Number(new URLSearchParams(requestBody).get('code'))
|
||
if (chosenCode) {
|
||
result.chosenReasonInUi = {
|
||
code: chosenCode,
|
||
text: reasonCodeToTextMap[chosenCode]
|
||
}
|
||
}
|
||
} else {
|
||
const cancelButtonProxy = await chooseReasonDialogProxy.$(`.zp-close`)
|
||
await cancelButtonProxy.click()
|
||
}
|
||
|
||
await sleepWithRandomDelay(2500)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
|
||
let isJobNameSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||
try {
|
||
if (expectJobNameRegExpStr.trim()) {
|
||
const regExp = new RegExp(expectJobNameRegExpStr, 'im')
|
||
isJobNameSuit = regExp.test(jobInfo.jobName?.replace(/\n/g, '') ?? '')
|
||
}
|
||
} catch {
|
||
}
|
||
let isJobTypeSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||
try {
|
||
if (expectJobTypeRegExpStr.trim()) {
|
||
const regExp = new RegExp(expectJobTypeRegExpStr, 'im')
|
||
isJobTypeSuit = regExp.test(jobInfo.positionName?.replace(/\n/g, '') ?? '')
|
||
}
|
||
} catch {
|
||
}
|
||
let isJobDescSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||
try {
|
||
if (expectJobDescRegExpStr.trim()) {
|
||
const regExp = new RegExp(expectJobDescRegExpStr, 'im')
|
||
isJobDescSuit = regExp.test(jobInfo.postDescription?.replace(/\n/g, '') ?? '')
|
||
}
|
||
} catch {
|
||
}
|
||
if (matchLogic === JobDetailRegExpMatchLogic.SOME) {
|
||
return isJobNameSuit || isJobTypeSuit || isJobDescSuit
|
||
}
|
||
else {
|
||
return isJobNameSuit && isJobTypeSuit && isJobDescSuit
|
||
}
|
||
}
|
||
|
||
async function setFilterCondition (selectedFilters) {
|
||
const {
|
||
cityList = [],
|
||
salaryList = [],
|
||
experienceList = [],
|
||
degreeList = [],
|
||
scaleList = [],
|
||
industryList = []
|
||
} = selectedFilters
|
||
|
||
const placeholderTexts = ['城市', '薪资待遇', '工作经验', '学历要求', '公司行业', '公司规模']
|
||
const optionKaPrefixes = ['switch_city_dialog_open', 'sel-job-rec-salary-', 'sel-job-rec-exp-', 'sel-job-rec-degree-', 'sel-industry-', 'sel-job-rec-scale-']
|
||
const conditionArr = [cityList, salaryList, experienceList, degreeList, industryList, scaleList]
|
||
|
||
console.log('current filter condition----')
|
||
for (let i = 0; i < placeholderTexts.length; i++) {
|
||
const text = placeholderTexts[i]
|
||
const condition = conditionArr[i]
|
||
console.log(`${text}:`, condition.length === 0 ? '不限' : condition.map(code => {
|
||
if (text === '公司行业') {
|
||
return industryFilterConditionsMapByCode[code]?.name ?? code
|
||
} else {
|
||
return jobFilterConditionsMapByCode[code]?.name ?? code
|
||
}
|
||
}).join(','))
|
||
}
|
||
console.log('----------------------------')
|
||
for(let i = 0; i < placeholderTexts.length; i++) {
|
||
const placeholderText = placeholderTexts[i]
|
||
const filterDropdownProxy = await (async () => {
|
||
const jsHandle = (await page.evaluateHandle((placeholderText) => {
|
||
if (placeholderText === '城市') {
|
||
return document.querySelector('.page-jobs-main .filter-condition-inner [ka="switch_city_dialog_open"]')
|
||
}
|
||
else {
|
||
const filterBar = document.querySelector('.page-jobs-main .filter-condition-inner')
|
||
const dropdownEntry = filterBar.__vue__.$children.find(it => it.placeholder === placeholderText)
|
||
return dropdownEntry?.$el
|
||
}
|
||
}, placeholderText))?.asElement();
|
||
return jsHandle
|
||
})()
|
||
if (!filterDropdownProxy) {
|
||
continue
|
||
}
|
||
|
||
const currentFilterConditions = conditionArr[i];
|
||
const filterDropdownCssList = await filterDropdownProxy.evaluate(el => Array.from(el.classList));
|
||
if (placeholderText === '城市') {
|
||
const onPageSelectedCity = filterDropdownCssList.includes('active') ? (await filterDropdownProxy.evaluate(el => el.textContent.trim())) : null
|
||
if (!onPageSelectedCity && !currentFilterConditions.length) {
|
||
continue
|
||
} else if (onPageSelectedCity === (currentFilterConditions[0] ?? null)) {
|
||
continue
|
||
} else {
|
||
if (!currentFilterConditions.length) {
|
||
const clearButtonHandle = await page.$(`.page-jobs-main .filter-condition-inner [ka="empty-filter"]`)
|
||
await clearButtonHandle.click()
|
||
}
|
||
else {
|
||
await filterDropdownProxy?.click()
|
||
await page.waitForFunction(() => {
|
||
const dialogEl = document.querySelector('.city-select-dialog')
|
||
return dialogEl && window.getComputedStyle(dialogEl).display !== 'none'
|
||
})
|
||
const citySelectWrapperProxy = await page.waitForSelector('.city-select-wrapper')
|
||
let targetCityElJsHandle = (await page.evaluateHandle((cityName) => {
|
||
const targetCityEl = [...document.querySelectorAll('.city-select-dialog .city-select-wrapper ul.city-list-hot li')].find(it => it.textContent.trim() === cityName) ?? null
|
||
return targetCityEl
|
||
}, currentFilterConditions[0]))?.asElement()
|
||
if (!targetCityElJsHandle) {
|
||
const targetCityItem = flattedCityList.find(it => it.name === currentFilterConditions[0])
|
||
if (!targetCityItem) {
|
||
// unexpected condition
|
||
continue
|
||
}
|
||
const firstChar = targetCityItem.firstChar
|
||
const targetCityCharListEntryHandle = await page.$(`xpath///*[contains(@class, "city-select-dialog")]//*[contains(@class, "city-select-wrapper")]//ul[contains(@class, "city-char-list")]//li[contains(text(), '${firstChar.toUpperCase()}')]`)
|
||
await targetCityCharListEntryHandle.click()
|
||
targetCityElJsHandle = (await page.evaluateHandle((cityName) => {
|
||
const targetCityEl = [...document.querySelectorAll('.city-select-dialog .city-select-wrapper .list-select-list a')].find(it => it.textContent.trim() === cityName) ?? null
|
||
return targetCityEl
|
||
}, currentFilterConditions[0]))?.asElement()
|
||
}
|
||
if (!targetCityElJsHandle) {
|
||
// unexpected condition
|
||
continue
|
||
}
|
||
await targetCityElJsHandle.click()
|
||
await sleep(1000)
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
if (!filterDropdownCssList.includes('is-select') && !currentFilterConditions.length) {
|
||
continue
|
||
} else {
|
||
await filterDropdownProxy.scrollIntoView()
|
||
const filterDropdownElBBox = await filterDropdownProxy.boundingBox()
|
||
await page.mouse.move(
|
||
filterDropdownElBBox.x + filterDropdownElBBox.width / 2,
|
||
filterDropdownElBBox.y + filterDropdownElBBox.height / 2,
|
||
)
|
||
await sleepWithRandomDelay(500)
|
||
|
||
const optionKaPrefix = optionKaPrefixes[i]
|
||
if (!currentFilterConditions.length) {
|
||
if (placeholderText === '公司行业') {
|
||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||
for (const it of activeOptionElAtCurrentFilterProxyList) {
|
||
await it.click()
|
||
}
|
||
} else {
|
||
// select 不限 immediately
|
||
const buxianOptionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${0}"]`)
|
||
await buxianOptionElProxy.click()
|
||
}
|
||
} else {
|
||
//#region uncheck options perviously checked but not existed in current filter.
|
||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||
const activeOptionValues = (await Promise.all(
|
||
activeOptionElAtCurrentFilterProxyList.map(elProxy => {
|
||
return elProxy.evaluate((el) => {
|
||
return el.getAttribute('ka')
|
||
})
|
||
})
|
||
)).map(it => it.replace(optionKaPrefix, '')).map(Number)
|
||
if (placeholderText !== '薪资待遇') {
|
||
for(let i = 0; i < activeOptionValues.length; i++) {
|
||
let activeValue
|
||
if (placeholderText === '公司行业') {
|
||
activeValue = industryFilterConditionsMapByIndex[activeOptionValues[i]]?.code
|
||
} else {
|
||
activeValue = activeOptionValues[i]
|
||
}
|
||
const activeOptionElProxy = activeOptionElAtCurrentFilterProxyList[i]
|
||
if (!currentFilterConditions.includes(activeValue)) {
|
||
await activeOptionElProxy.click()
|
||
}
|
||
}
|
||
}
|
||
//#endregion
|
||
//#region only click the one which we need check, don't change already checked.
|
||
const conditionToCheck = currentFilterConditions.filter(it => {
|
||
if (placeholderText === '公司行业') {
|
||
return !activeOptionValues.map(value => industryFilterConditionsMapByIndex[value].code).includes(it);
|
||
} else {
|
||
return !activeOptionValues.includes(it)
|
||
}
|
||
})
|
||
for(let j = 0; j < conditionToCheck.length; j++) {
|
||
let optionValue
|
||
if (placeholderText === '公司行业') {
|
||
optionValue = industryFilterConditionCodeToIndexMap[conditionToCheck[j]]
|
||
} else {
|
||
optionValue = conditionToCheck[j]
|
||
}
|
||
await sleepWithRandomDelay(500)
|
||
await filterDropdownProxy.scrollIntoView()
|
||
const filterDropdownElBBox = await filterDropdownProxy.boundingBox()
|
||
await page.mouse.move(
|
||
filterDropdownElBBox.x + filterDropdownElBBox.width / 2,
|
||
filterDropdownElBBox.y + filterDropdownElBBox.height / 2,
|
||
)
|
||
await sleepWithRandomDelay(500)
|
||
const optionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${optionValue}"]`)
|
||
if (!optionElProxy) {
|
||
continue;
|
||
}
|
||
await optionElProxy.click()
|
||
}
|
||
//#endregion
|
||
//#region move out dropdown entry to make dropdown hidden
|
||
const navBarLogoElProxy = await page.$(`[ka="header-home-logo"]`)
|
||
if (navBarLogoElProxy) {
|
||
const navBarLogoElBBox = await navBarLogoElProxy.boundingBox()
|
||
await page.mouse.move(
|
||
navBarLogoElBBox.x + navBarLogoElBBox.width / 2,
|
||
navBarLogoElBBox.y + navBarLogoElBBox.height / 2,
|
||
)
|
||
}
|
||
//#endregion
|
||
}
|
||
await sleepWithRandomDelay(500)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function toRecommendPage (hooks) {
|
||
let userInfoPromise = page.waitForResponse((response) => {
|
||
if (response.url().startsWith('https://www.zhipin.com/wapi/zpuser/wap/getUserInfo.json')) {
|
||
return true
|
||
}
|
||
return false
|
||
}, { timeout: 120 * 1000 }).then((res) => {
|
||
return res.json()
|
||
})
|
||
page.goto(recommendJobPageUrl, { timeout: 1 * 1000 }).catch(e => { void e })
|
||
await sleep(3000)
|
||
await page.waitForFunction(() => {
|
||
return document.readyState === 'complete'
|
||
}, { timeout: 120 * 1000 })
|
||
if (
|
||
page.url().startsWith('https://www.zhipin.com/web/common/403.html') ||
|
||
page.url().startsWith('https://www.zhipin.com/web/common/error.html')
|
||
) {
|
||
throw new Error("ACCESS_IS_DENIED")
|
||
}
|
||
|
||
await page.waitForFunction(({ recommendJobPageUrl }) => {
|
||
return location.href.startsWith(recommendJobPageUrl) && document.readyState === 'complete'
|
||
}, undefined, { recommendJobPageUrl })
|
||
|
||
hooks.pageLoaded?.call()
|
||
|
||
let userInfoResponse = await userInfoPromise
|
||
await hooks.userInfoResponse?.promise(userInfoResponse)
|
||
if (userInfoResponse?.code !== 0) {
|
||
autoStartChatEventBus.emit('LOGIN_STATUS_INVALID', {
|
||
userInfoResponse
|
||
})
|
||
writeStorageFile('boss-cookies.json', [])
|
||
throw new Error("LOGIN_STATUS_INVALID")
|
||
} else {
|
||
await storeStorage(page).catch(() => void 0)
|
||
}
|
||
|
||
const computedSourceList = []
|
||
for (const source of normalizedJobSource) {
|
||
switch (source.type) {
|
||
case 'recommend': {
|
||
computedSourceList.push({
|
||
type: source.type,
|
||
selector: RECOMMEND_JOB_ENTRY_SELECTOR,
|
||
async getIsCurrentActiveSource () {
|
||
return await page.evaluate(
|
||
({ RECOMMEND_JOB_ENTRY_SELECTOR }) => {
|
||
return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active')
|
||
}, {
|
||
RECOMMEND_JOB_ENTRY_SELECTOR
|
||
}
|
||
)
|
||
},
|
||
async setToActiveSource() {
|
||
// not first navigation and should choose a job (except job)
|
||
// click first expect job
|
||
const expectJobTabHandler = await page.$(RECOMMEND_JOB_ENTRY_SELECTOR)
|
||
await expectJobTabHandler.click() // switch to first condition
|
||
}
|
||
})
|
||
continue
|
||
}
|
||
case 'expect': {
|
||
await page.waitForSelector(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
|
||
const allExpectJobEntryHandles = await page.$$(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
|
||
allExpectJobEntryHandles.forEach((it, index) => {
|
||
computedSourceList.push({
|
||
type: source.type,
|
||
selector: `${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`,
|
||
async getIsCurrentActiveSource () {
|
||
return await page.evaluate(
|
||
({
|
||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
|
||
index
|
||
}) => {
|
||
return document.querySelector(`${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`).classList.contains('active')
|
||
}, {
|
||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
|
||
index
|
||
}
|
||
)
|
||
},
|
||
async setToActiveSource() {
|
||
// not first navigation and should choose a job (except job)
|
||
// click first expect job
|
||
const expectJobTabHandler = await page.$(`${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`)
|
||
await expectJobTabHandler.click() // switch to first condition
|
||
}
|
||
})
|
||
})
|
||
break
|
||
}
|
||
case 'search': {
|
||
computedSourceList.push({
|
||
type: source.type,
|
||
async getIsCurrentActiveSource () {
|
||
const elHandle = await page.$(`.page-jobs-main`)
|
||
const currentKeyWord = await elHandle?.evaluate((el) => {
|
||
return el?.__vue__?.formData?.query
|
||
})
|
||
if (!currentKeyWord) {
|
||
return false
|
||
}
|
||
return currentKeyWord === source.keyword
|
||
},
|
||
async setToActiveSource() {
|
||
await page.waitForSelector(SEARCH_BOX_SELECTOR)
|
||
const inputHandle = await page.$(`${SEARCH_BOX_SELECTOR} input`)
|
||
await inputHandle.focus()
|
||
await sleep(100)
|
||
let currentValue = await inputHandle.evaluate(el => el.value)
|
||
while (currentValue) {
|
||
await inputHandle.press('Backspace')
|
||
currentValue = await inputHandle.evaluate(el => el.value)
|
||
}
|
||
await inputHandle.type(source.keyword?.trim() || '', { delay: 100 })
|
||
await sleep(500)
|
||
await inputHandle.press('Enter')
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
let currentSourceIndex = 0
|
||
afterPageLoad: while (true) {
|
||
// check set security question tip modal
|
||
let setSecurityQuestionTipModelProxy
|
||
try {
|
||
setSecurityQuestionTipModelProxy = await page.waitForSelector('.dialog-wrap.dialog-account-safe', { timeout: 3 * 1000 })
|
||
}
|
||
catch(err) {
|
||
console.log(`cannot find set security question tip modal, just continue`)
|
||
}
|
||
if (
|
||
setSecurityQuestionTipModelProxy
|
||
) {
|
||
await sleep(1000)
|
||
setSecurityQuestionTipModelProxy = await page.$('.dialog-wrap.dialog-account-safe')
|
||
const closeButtonProxy = await setSecurityQuestionTipModelProxy?.$('.close')
|
||
|
||
if (setSecurityQuestionTipModelProxy && closeButtonProxy) {
|
||
await closeButtonProxy.click()
|
||
}
|
||
}
|
||
|
||
const filterConditions =
|
||
combineRecommendJobFilterType === CombineRecommendJobFilterType.STATIC_COMBINE
|
||
? formatStaticCombineFilters(staticCombineRecommendJobFilterConditions)
|
||
: combineFiltersWithConstraintsGenerator(anyCombineRecommendJobFilter)
|
||
let expectJobList
|
||
let filterConditionIndex = -1
|
||
iterateFilterCondition: for (
|
||
const filterCondition of filterConditions
|
||
) {
|
||
filterConditionIndex++
|
||
console.log(`current filter condition index to apply: ${filterConditionIndex}`, JSON.stringify(filterCondition))
|
||
findInCurrentFilterCondition: while(true) {
|
||
await sleepWithRandomDelay(2500)
|
||
|
||
await Promise.all([
|
||
Promise.race([
|
||
page.waitForSelector(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR),
|
||
page.waitForSelector(RECOMMEND_JOB_ENTRY_SELECTOR),
|
||
]),
|
||
Promise.race([
|
||
page.waitForSelector(".job-list-container .rec-job-list"),
|
||
page.waitForSelector(".recommend-result-job .job-empty-wrapper")
|
||
])
|
||
])
|
||
// await page.click(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
|
||
await sleep(3000)
|
||
let onPageCurrentSourceIndex = -1
|
||
for (let i=0; i < computedSourceList.length; i++) {
|
||
const computedSource = computedSourceList[i]
|
||
if (await computedSource.getIsCurrentActiveSource()) {
|
||
onPageCurrentSourceIndex = i
|
||
break
|
||
}
|
||
}
|
||
if (
|
||
(
|
||
combineRecommendJobFilterType === CombineRecommendJobFilterType.STATIC_COMBINE && filterCondition === null
|
||
)
|
||
||
|
||
(
|
||
combineRecommendJobFilterType === CombineRecommendJobFilterType.ANY_COMBINE &&
|
||
isSkipEmptyConditionForCombineRecommendJobFilter &&
|
||
Object.keys(filterCondition).length &&
|
||
Object.keys(filterCondition).every(k => !filterCondition[k]?.length)
|
||
)
|
||
) {
|
||
sleep(4000)
|
||
continue iterateFilterCondition
|
||
}
|
||
expectJobList = await page.evaluate(`document.querySelector('.c-expect-select')?.__vue__?.expectList`)
|
||
if (onPageCurrentSourceIndex === currentSourceIndex) {
|
||
// first navigation and can immediately start chat (recommend job)
|
||
} else {
|
||
await computedSourceList[currentSourceIndex].setToActiveSource()
|
||
await page.waitForResponse(
|
||
response => {
|
||
if (
|
||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json') ||
|
||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/search/joblist.json')
|
||
) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
);
|
||
await storeStorage(page).catch(() => void 0)
|
||
await sleepWithRandomDelay(2000)
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'afterJobSourceChosen',
|
||
hooks
|
||
})
|
||
}
|
||
await sleepWithRandomDelay(1500)
|
||
await setFilterCondition(filterCondition)
|
||
await sleep(1500) // TODO: accurately check if job list request sent and response received after set condition
|
||
await page.waitForFunction(() => {
|
||
return !document.querySelector('.job-recommend-result .job-rec-loading')
|
||
})
|
||
try {
|
||
const { targetJobIndex, targetJobData } = await new Promise(async (resolve, reject) => {
|
||
try {
|
||
let requestNextPagePromiseWithResolver = null
|
||
page.on(
|
||
'request',
|
||
function reqHandler (request) {
|
||
if (
|
||
request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json') ||
|
||
request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/search/joblist.json')
|
||
) {
|
||
requestNextPagePromiseWithResolver = (() => {
|
||
const o = {}
|
||
o.promise = new Promise((resolve, reject) => {
|
||
o.resolve = resolve
|
||
o.reject = reject
|
||
})
|
||
return o
|
||
})()
|
||
page.off(reqHandler)
|
||
|
||
page.on(
|
||
'response',
|
||
function resHandler (response) {
|
||
if (response.request() === request) {
|
||
requestNextPagePromiseWithResolver?.resolve()
|
||
page.off(resHandler)
|
||
}
|
||
}
|
||
)
|
||
}
|
||
}
|
||
)
|
||
// job list
|
||
let recommendJobListElProxy
|
||
try {
|
||
recommendJobListElProxy= await page.waitForSelector('.job-list-container .rec-job-list', { timeout: 5 * 1000 })
|
||
} catch {}
|
||
if (!recommendJobListElProxy){
|
||
await hooks.encounterEmptyRecommendJobList?.promise({
|
||
pageQuery: await page.evaluate(() => new URL(location.href).searchParams.toString())
|
||
})
|
||
throw new Error('CANNOT_FIND_EXCEPT_JOB_IN_THIS_FILTER_CONDITION')
|
||
}
|
||
let jobListData = []
|
||
async function updateJobListData () {
|
||
jobListData = await page.evaluate(`document.querySelector('.page-jobs-main')?.__vue__?.jobList`)
|
||
// due to city can get from list immediately
|
||
// so just set those job which city is not suit to blockJobNotSuit
|
||
// to skip view detail
|
||
|
||
// skip invalid salaryData (兼职、日结、实习 etc)
|
||
jobListData.forEach(it => {
|
||
const salaryData = parseSalary(it.salaryDesc)
|
||
if (!salaryData.high || !salaryData.low) {
|
||
blockJobNotSuit.add(it.encryptJobId)
|
||
}
|
||
})
|
||
if (
|
||
(
|
||
expectCityNotMatchStrategy === MarkAsNotSuitOp.NO_OP &&
|
||
Array.isArray(expectCityList) &&
|
||
expectCityList.length
|
||
) ||
|
||
(
|
||
expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.NO_OP &&
|
||
Array.isArray(expectWorkExpList) &&
|
||
expectWorkExpList.length
|
||
) ||
|
||
(
|
||
strategyScopeOptionWhenMarkSalaryNotMatch === MarkAsNotSuitOp.NO_OP &&
|
||
isSalaryFilterEnabled
|
||
)
|
||
) {
|
||
console.log(`add job city not suit into blockJobNotSuit set`)
|
||
for (const it of jobListData) {
|
||
if (!expectCityList.includes(it.cityName)) {
|
||
blockJobNotSuit.add(it.encryptJobId)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
await updateJobListData()
|
||
|
||
let hasReachLastPage = false
|
||
let targetJobIndex = -1
|
||
let targetJobData, selectedJobData // they show be same; one is from list, another is from detail
|
||
function checkIfSalarySuit(salaryDesc) {
|
||
const salaryData = parseSalary(salaryDesc)
|
||
if (expectSalaryCalculateWay === SalaryCalculateWay.MONTH_SALARY) {
|
||
let ourSalaryInterval = [expectSalaryLow ?? null, expectSalaryHigh ?? null]
|
||
if (ourSalaryInterval.every(it => !isNaN(parseFloat(it)))) {
|
||
ourSalaryInterval = ourSalaryInterval.sort((a, b) => a - b)
|
||
}
|
||
const theirSalaryInterval = [salaryData.low ?? null, salaryData.high ?? null]
|
||
return hasIntersection(theirSalaryInterval, ourSalaryInterval)
|
||
}
|
||
else if (expectSalaryCalculateWay === SalaryCalculateWay.ANNUAL_PACKAGE) {
|
||
const salaryDataMonth = salaryData.month || 12
|
||
let ourSalaryInterval = [expectSalaryLow ?? null, expectSalaryHigh ?? null]
|
||
if (ourSalaryInterval.every(it => !isNaN(parseFloat(it)))) {
|
||
ourSalaryInterval = ourSalaryInterval.sort((a, b) => a - b)
|
||
}
|
||
const theirSalaryInterval = [salaryData.low ?? null, salaryData.high ?? null].map(
|
||
it =>
|
||
it === null ? null : (it * salaryDataMonth / 10)
|
||
)
|
||
return hasIntersection(theirSalaryInterval, ourSalaryInterval)
|
||
}
|
||
return true
|
||
}
|
||
function getTempTargetJobIndexToCheckDetail () {
|
||
return jobListData.findIndex(it => {
|
||
return !blockBossNotNewChat.has(it.encryptBossId) &&
|
||
!blockBossNotActive.has(it.encryptBossId) &&
|
||
!blockJobNotSuit.has(it.encryptJobId) &&
|
||
(
|
||
(
|
||
enableCompanyAllowList ?
|
||
[...expectCompanySet].find(
|
||
name => it.brandName?.toLowerCase?.()?.includes(name.toLowerCase())
|
||
)
|
||
:
|
||
true
|
||
) || (
|
||
// enter job detail to mark as not suit for city filter
|
||
(
|
||
Array.isArray(expectCityList) &&
|
||
expectCityList.length &&
|
||
[
|
||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||
].includes(expectCityNotMatchStrategy) &&
|
||
strategyScopeOptionWhenMarkJobCityNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
|
||
) ? !expectCityList.includes(it.cityName) : false
|
||
) || (
|
||
// enter job detail to mark as not suit for work exp filter
|
||
(
|
||
Array.isArray(expectWorkExpList) &&
|
||
expectWorkExpList.length &&
|
||
[
|
||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||
].includes(expectWorkExpNotMatchStrategy) &&
|
||
strategyScopeOptionWhenMarkJobWorkExpNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
|
||
) ? !expectWorkExpList.includes(it.jobExperience) : false
|
||
) || (
|
||
// enter job detail to mark as not suit for salary filter
|
||
(
|
||
isSalaryFilterEnabled &&
|
||
[
|
||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||
].includes(expectSalaryNotMatchStrategy) &&
|
||
strategyScopeOptionWhenMarkSalaryNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
|
||
) ? !checkIfSalarySuit(it.salaryDesc) : false
|
||
) || (
|
||
// enter job detail to mark as not suit for company name filter
|
||
!!blockCompanyNameRegExp &&
|
||
blockCompanyNameRegExp.test(it.brandName?.toLowerCase?.() ?? '') &&
|
||
[
|
||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||
].includes(blockCompanyNameRegMatchStrategy)
|
||
)
|
||
)
|
||
})
|
||
}
|
||
continueFind: while (targetJobIndex < 0 && !hasReachLastPage) {
|
||
// when disable company allow list, we will believe that the first one in the list is your expect job.
|
||
let tempTargetJobIndexToCheckDetail = getTempTargetJobIndexToCheckDetail()
|
||
while (tempTargetJobIndexToCheckDetail < 0 && !hasReachLastPage) {
|
||
// fetch new
|
||
const recommendJobListElBBox = await recommendJobListElProxy.boundingBox()
|
||
const windowInnerHeight = await page.evaluate('window.innerHeight')
|
||
await page.mouse.move(
|
||
recommendJobListElBBox.x + recommendJobListElBBox.width / 2,
|
||
windowInnerHeight / 2
|
||
)
|
||
let scrolledHeight = 0
|
||
const increase = 40 + Math.floor(30 * Math.random())
|
||
|
||
while (
|
||
!requestNextPagePromiseWithResolver &&
|
||
!hasReachLastPage
|
||
) {
|
||
scrolledHeight += increase
|
||
await page.mouse.wheel({deltaY: increase});
|
||
await sleep(100)
|
||
await requestNextPagePromiseWithResolver?.promise
|
||
hasReachLastPage = await page.evaluate(`
|
||
!(document.querySelector('.page-jobs-main')?.__vue__?.hasMore)
|
||
`)
|
||
if (hasReachLastPage) {
|
||
console.log(`Arrive the terminal of the job list.`)
|
||
}
|
||
}
|
||
requestNextPagePromiseWithResolver = null
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'afterJobListPageFetched',
|
||
hooks
|
||
})
|
||
await sleep(5000)
|
||
await updateJobListData()
|
||
tempTargetJobIndexToCheckDetail = getTempTargetJobIndexToCheckDetail()
|
||
}
|
||
|
||
if (tempTargetJobIndexToCheckDetail < 0 && hasReachLastPage) {
|
||
// has reach last page and not find target job
|
||
reject(new Error('CANNOT_FIND_EXCEPT_JOB_IN_THIS_FILTER_CONDITION'))
|
||
return
|
||
}
|
||
|
||
//#region here to check detail
|
||
if (tempTargetJobIndexToCheckDetail >= 0) {
|
||
// scroll that target element into view
|
||
await page.evaluate(`
|
||
targetEl = document.querySelector("ul.rec-job-list").children[${tempTargetJobIndexToCheckDetail}]
|
||
targetEl.scrollIntoView({
|
||
behavior: 'smooth',
|
||
block: ${Math.random() > 0.5 ? '\'center\'' : '\'end\''}
|
||
})
|
||
`)
|
||
|
||
await sleepWithRandomDelay(200)
|
||
|
||
if (tempTargetJobIndexToCheckDetail === 0) {
|
||
} else {
|
||
const recommendJobItemList = await recommendJobListElProxy.$$('ul.rec-job-list li.job-card-box')
|
||
const targetJobElProxy = recommendJobItemList[tempTargetJobIndexToCheckDetail]
|
||
// click that element
|
||
await sleep(500)
|
||
await targetJobElProxy.click()
|
||
await page.waitForResponse(
|
||
response => {
|
||
if (
|
||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')
|
||
) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
);
|
||
await sleepWithRandomDelay(2000)
|
||
}
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'afterJobDetailFetched',
|
||
hooks
|
||
})
|
||
targetJobData = await page.evaluate('document.querySelector(".job-detail-box").__vue__.data')
|
||
selectedJobData = await page.evaluate('document.querySelector(".page-jobs-main").__vue__.currentJob')
|
||
// save the job detail info
|
||
await hooks.jobDetailIsGetFromRecommendList?.promise(targetJobData)
|
||
|
||
//#region collect not suit reasons
|
||
const notSuitReasonIdToStrategyMap = {}
|
||
const notSuitConditionHandleMap = {
|
||
async companyName() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (blockCompanyNameRegMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL && !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||
try {
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT,
|
||
extInfo: null,
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (blockCompanyNameRegMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobNotSuitMarked',
|
||
hooks
|
||
})
|
||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT)
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT,
|
||
extInfo: {
|
||
chosenReasonInUi
|
||
},
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch(err) {
|
||
console.log(`mark boss inactive failed`, err)
|
||
}
|
||
}
|
||
},
|
||
async active() {
|
||
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
|
||
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||
try {
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||
extInfo: null,
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobNotSuitMarked',
|
||
hooks
|
||
})
|
||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.BOSS_INACTIVE)
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||
extInfo: {
|
||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||
chosenReasonInUi
|
||
},
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch(err) {
|
||
console.log(`mark boss inactive failed`, err)
|
||
}
|
||
}
|
||
},
|
||
async city() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||
try {
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_CITY_NOT_SUIT,
|
||
extInfo: null,
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobNotSuitMarked',
|
||
hooks
|
||
})
|
||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_CITY_NOT_SUIT,
|
||
extInfo: {
|
||
chosenReasonInUi
|
||
},
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch(err) {
|
||
console.log(`mark job city not suit failed`, err)
|
||
}
|
||
}
|
||
},
|
||
async workExp() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||
try {
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT,
|
||
extInfo: null,
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobNotSuitMarked',
|
||
hooks
|
||
})
|
||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT)
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT,
|
||
extInfo: {
|
||
chosenReasonInUi
|
||
},
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch(err) {
|
||
console.log(`mark job work exp not suit failed`, err)
|
||
}
|
||
}
|
||
},
|
||
async jobDetail() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||
try {
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||
extInfo: null,
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobNotSuitMarked',
|
||
hooks
|
||
})
|
||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_NOT_SUIT)
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||
extInfo: {
|
||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||
chosenReasonInUi
|
||
},
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch(err) {
|
||
console.log(`mark job detail not suit failed`, err)
|
||
}
|
||
}
|
||
},
|
||
async salary() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||
try {
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT,
|
||
extInfo: {
|
||
salaryDesc: selectedJobData.salaryDesc,
|
||
},
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobNotSuitMarked',
|
||
hooks
|
||
})
|
||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT)
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT,
|
||
extInfo: {
|
||
salaryDesc: selectedJobData.salaryDesc,
|
||
chosenReasonInUi
|
||
},
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
} catch(err) {
|
||
console.log(`mark job salary not suit failed`, err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (
|
||
!!blockCompanyNameRegExp && blockCompanyNameRegExp.test(selectedJobData.brandName ?? '')
|
||
) {
|
||
notSuitReasonIdToStrategyMap.companyName = blockCompanyNameRegMatchStrategy
|
||
}
|
||
//#region
|
||
// null
|
||
// 刚刚活跃 // 今日活跃 // 昨日活跃 // 3日内活跃 // 本周活跃 // 2周内活跃
|
||
// 本月活跃 // 2月内活跃 // 3月内活跃 // 4月内活跃 // 5月内活跃 // 近半年活跃 // 半年前活跃
|
||
//#endregion
|
||
let activeTimeDescForCompare = targetJobData.bossInfo.activeTimeDesc
|
||
// handle empty string case
|
||
if (activeTimeDescForCompare === '') {
|
||
activeTimeDescForCompare = '半年前活跃'
|
||
}
|
||
const indexOfActiveText = activeDescList.indexOf(activeTimeDescForCompare)
|
||
if (
|
||
markAsNotActiveSelectedTimeRange > 0 &&
|
||
indexOfActiveText > 0 && indexOfActiveText <= markAsNotActiveSelectedTimeRange
|
||
) {
|
||
// click prevent recommend button
|
||
notSuitReasonIdToStrategyMap.active = jobNotActiveStrategy
|
||
}
|
||
if (
|
||
(Array.isArray(expectCityList) && expectCityList.length) && !expectCityList.includes(selectedJobData.cityName)
|
||
) {
|
||
notSuitReasonIdToStrategyMap.city = expectCityNotMatchStrategy
|
||
}
|
||
if (
|
||
(Array.isArray(expectWorkExpList) && expectWorkExpList.length) && !expectWorkExpList.includes(selectedJobData.jobExperience)
|
||
) {
|
||
notSuitReasonIdToStrategyMap.workExp = expectWorkExpNotMatchStrategy
|
||
}
|
||
if (
|
||
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo, jobDetailRegExpMatchLogic)
|
||
) {
|
||
notSuitReasonIdToStrategyMap.jobDetail = jobNotMatchStrategy
|
||
}
|
||
if (
|
||
!checkIfSalarySuit(selectedJobData.salaryDesc)
|
||
) {
|
||
notSuitReasonIdToStrategyMap.salary = expectSalaryNotMatchStrategy
|
||
}
|
||
// #endregion
|
||
console.log('not suit reason and related strategy: ', notSuitReasonIdToStrategyMap)
|
||
|
||
// #region execute mark logic
|
||
// 1. find the one mark on Boss
|
||
const markOnBossCondition = Object.keys(notSuitReasonIdToStrategyMap).find(k => notSuitReasonIdToStrategyMap[k] === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS)
|
||
if (markOnBossCondition) {
|
||
await notSuitConditionHandleMap[markOnBossCondition]()
|
||
continue continueFind
|
||
}
|
||
// 2. if there is no condition to mark Boss, then find the one mark on local db
|
||
const markOnLocalDbCondition = Object.keys(notSuitReasonIdToStrategyMap).find(k => notSuitReasonIdToStrategyMap[k] === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL)
|
||
if (markOnLocalDbCondition) {
|
||
await notSuitConditionHandleMap[markOnLocalDbCondition]()
|
||
continue continueFind
|
||
}
|
||
// 3.
|
||
const noOpCondition = Object.keys(notSuitReasonIdToStrategyMap).find(k => notSuitReasonIdToStrategyMap[k] === MarkAsNotSuitOp.NO_OP)
|
||
if (noOpCondition) {
|
||
await notSuitConditionHandleMap[noOpCondition]()
|
||
continue continueFind
|
||
}
|
||
// #endregion
|
||
if (
|
||
// test company again - when allow list not include target company, just skip
|
||
enableCompanyAllowList && ![...expectCompanySet].find(
|
||
name => selectedJobData.brandName?.toLowerCase?.()?.includes(name.toLowerCase())
|
||
) ||
|
||
// check if job has been marked as not suit or not active
|
||
[
|
||
...blockJobNotSuit,
|
||
...blockBossNotActive
|
||
].includes(targetJobData.jobInfo.encryptId)
|
||
) {
|
||
// just skip
|
||
continue continueFind
|
||
}
|
||
const startChatButtonInnerHTML = await page.evaluate('document.querySelector(".job-detail-box .op-btn.op-btn-chat")?.innerHTML.trim()')
|
||
if (startChatButtonInnerHTML !== '立即沟通') {
|
||
blockBossNotNewChat.add(targetJobData.jobInfo.encryptUserId)
|
||
continue continueFind
|
||
}
|
||
targetJobIndex = tempTargetJobIndexToCheckDetail
|
||
//#endregion
|
||
}
|
||
|
||
if (targetJobIndex < 0 && hasReachLastPage) {
|
||
// has reach last page and not find target job
|
||
reject(new Error('CANNOT_FIND_EXCEPT_JOB_IN_THIS_FILTER_CONDITION'))
|
||
return
|
||
}
|
||
}
|
||
|
||
resolve(
|
||
{
|
||
targetJobIndex,
|
||
targetJobData
|
||
}
|
||
)
|
||
} catch(err) {
|
||
reject(err)
|
||
}
|
||
})
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobChatStartup',
|
||
hooks
|
||
})
|
||
await sleepWithRandomDelay(1000)
|
||
const startChatButtonInnerHTML = await page.evaluate('document.querySelector(".job-detail-box .op-btn.op-btn-chat")?.innerHTML.trim()')
|
||
|
||
await hooks.newChatWillStartup?.promise(targetJobData)
|
||
const startChatButtonProxy = await page.$('.job-detail-box .op-btn.op-btn-chat')
|
||
await sleep(500)
|
||
//#region click the chat button
|
||
await startChatButtonProxy.click()
|
||
|
||
const waitAddFriendResponse = async () => {
|
||
const addFriendResponse = await page.waitForResponse(
|
||
response => {
|
||
if (
|
||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/friend/add.json') && response.url().includes(`jobId=${targetJobData.jobInfo.encryptId}`)
|
||
) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
);
|
||
await sleepWithRandomDelay(3000)
|
||
let res
|
||
try {
|
||
res = await addFriendResponse.json()
|
||
return res
|
||
}
|
||
catch(err) {
|
||
await sleep(2000)
|
||
if (page.url().startsWith('https://www.zhipin.com/web/geek/chat')) {
|
||
throw new Error('PAGE_JUMPED_TO_CHAT_PAGE')
|
||
}
|
||
else {
|
||
throw err
|
||
}
|
||
}
|
||
}
|
||
const waitAndHandleChatSuccess = async ({ hasGoToChatPage = false } = {}) => {
|
||
await hooks.newChatStartup?.promise(
|
||
targetJobData,
|
||
{
|
||
chatStartupFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||
}
|
||
)
|
||
blockBossNotNewChat.add(targetJobData.jobInfo.encryptUserId)
|
||
|
||
await storeStorage(page).catch(() => void 0)
|
||
await sleepWithRandomDelay(1500)
|
||
if (hasGoToChatPage) {
|
||
await page.goBack()
|
||
await page.waitForFunction(() => {
|
||
return location.href.startsWith(`https://www.zhipin.com/web/geek/jobs`) && document.readyState === 'complete'
|
||
})
|
||
await sleepWithRandomDelay(2000)
|
||
}
|
||
const closeDialogButtonProxy = await page.$('.greet-boss-dialog .greet-boss-footer .cancel-btn')
|
||
if (closeDialogButtonProxy) {
|
||
await closeDialogButtonProxy.click()
|
||
await sleepWithRandomDelay(2000)
|
||
}
|
||
}
|
||
const handleAddFriendResponse = async (res, { hasGoToChatPage = false } = {}) => {
|
||
if (res.code === 0) {
|
||
await waitAndHandleChatSuccess({ hasGoToChatPage })
|
||
}
|
||
else if (
|
||
res.zpData.bizCode === 1 &&
|
||
res.zpData.bizData?.chatRemindDialog?.blockLevel === 0 &&
|
||
/剩\d+次沟通机会/.test(res.zpData.bizData?.chatRemindDialog?.content)
|
||
) {
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobChatStartupAfterTwiceConfirm',
|
||
hooks
|
||
})
|
||
const confirmButton = await page.waitForSelector('.chat-block-dialog .chat-block-footer .sure-btn')
|
||
await confirmButton.click()
|
||
const nextRes = await waitAddFriendResponse()
|
||
await handleAddFriendResponse(nextRes)
|
||
}
|
||
else if (
|
||
res.zpData.bizCode === 1 &&
|
||
/猎头/.test(res.zpData.bizData?.chatRemindDialog?.content)
|
||
) {
|
||
await waitForSageTimeOrJustContinue({
|
||
tag: 'beforeJobChatStartupAfterTwiceConfirm',
|
||
hooks
|
||
})
|
||
const confirmButton = await page.waitForSelector(`xpath///*[contains(@class, "chat-block-dialog")]//*[contains(@class, "chat-block-footer")]//*[contains(text(), "继续")]`)
|
||
await confirmButton.click()
|
||
const nextRes = await waitAddFriendResponse()
|
||
await handleAddFriendResponse(nextRes)
|
||
}
|
||
else if (
|
||
res.zpData.bizCode === 1 &&
|
||
res.zpData.bizData?.chatRemindDialog?.blockLevel === 0 &&
|
||
(
|
||
res.zpData.bizData?.chatRemindDialog?.content === `今日沟通人数已达上限,请明天再试` ||
|
||
/明天再来/.test(res.zpData.bizData?.chatRemindDialog?.content)
|
||
)
|
||
) {
|
||
// startup chat error, may the chance of today has used out
|
||
await storeStorage(page).catch(() => void 0)
|
||
throw new Error('STARTUP_CHAT_ERROR_DUE_TO_TODAY_CHANCE_HAS_USED_OUT')
|
||
}
|
||
else {
|
||
console.error(
|
||
JSON.stringify(res, null, 2)
|
||
)
|
||
throw new Error('STARTUP_CHAT_ERROR_WITH_UNKNOWN_ERROR')
|
||
}
|
||
}
|
||
let res
|
||
try {
|
||
res = await waitAddFriendResponse()
|
||
await handleAddFriendResponse(res)
|
||
}
|
||
catch (err) {
|
||
if (err instanceof Error && err.message === 'PAGE_JUMPED_TO_CHAT_PAGE') {
|
||
await handleAddFriendResponse({
|
||
code: 0
|
||
}, { hasGoToChatPage: true })
|
||
}
|
||
else {
|
||
throw err
|
||
}
|
||
}
|
||
} catch (err) {
|
||
if (err instanceof Error) {
|
||
switch (err.message) {
|
||
case 'CANNOT_FIND_EXCEPT_JOB_IN_THIS_FILTER_CONDITION': {
|
||
await sleepWithRandomDelay(25 * 1000)
|
||
continue iterateFilterCondition;
|
||
}
|
||
case 'STARTUP_CHAT_ERROR_DUE_TO_TODAY_CHANCE_HAS_USED_OUT': {
|
||
let nextTrySeconds = 60 * 60
|
||
const msg = `Today chance has used out. Just explore positions you\'ve chatted. New chat will be tried to start after ${nextTrySeconds} seconds.`
|
||
hooks.errorEncounter?.call(msg)
|
||
console.error(msg)
|
||
await sleep(nextTrySeconds * 1000)
|
||
throw err
|
||
}
|
||
case 'STARTUP_CHAT_ERROR_WITH_UNKNOWN_ERROR': {
|
||
hooks.errorEncounter?.call([err.message, err.stack].join('\n'))
|
||
throw err
|
||
}
|
||
default: {
|
||
hooks.errorEncounter?.call([err.message, err.stack].join('\n'))
|
||
throw err
|
||
}
|
||
}
|
||
} else {
|
||
hooks.errorEncounter?.call(err)
|
||
throw err
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// for of reach terminal
|
||
if (
|
||
currentSourceIndex + 1 >= computedSourceList.length
|
||
) {
|
||
hooks.noPositionFoundForCurrentJob?.call()
|
||
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
||
await sleep((20 + 30 * Math.random()) * 1000)
|
||
await Promise.all([
|
||
page.goto(`https://www.zhipin.com/web/geek/jobs`),
|
||
page.waitForNavigation()
|
||
])
|
||
currentSourceIndex = 0
|
||
} else {
|
||
hooks.noPositionFoundForCurrentJob?.call()
|
||
await sleep((10 + 15 * Math.random()) * 1000)
|
||
currentSourceIndex += 1
|
||
}
|
||
}
|
||
}
|
||
|
||
export async function mainLoop (hooks) {
|
||
if (!puppeteer) {
|
||
await initPuppeteer()
|
||
}
|
||
try {
|
||
browser = await puppeteer.launch({
|
||
headless: false,
|
||
ignoreHTTPSErrors: true,
|
||
defaultViewport: {
|
||
width: 1440,
|
||
height: 900 - 140,
|
||
}
|
||
})
|
||
hooks.puppeteerLaunched?.call(browser)
|
||
page = (await browser.pages())[0]
|
||
hooks.pageGotten?.call(page)
|
||
//set cookies
|
||
const bossCookies = readStorageFile('boss-cookies.json')
|
||
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||
await hooks.cookieWillSet?.promise(bossCookies)
|
||
for(let i = 0; i < bossCookies.length; i++){
|
||
if (Object.hasOwn(bossCookies[i], 'sameSite')) {
|
||
bossCookies[i].sameSite = 'unspecified'
|
||
}
|
||
await page.setCookie(bossCookies[i]);
|
||
}
|
||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
|
||
await page.bringToFront()
|
||
// __GGR_INJECT_ANTI_ANTI_DEBUGGER__
|
||
await hooks.mainFlowWillLaunch?.promise({
|
||
jobNotMatchStrategy,
|
||
jobNotActiveStrategy,
|
||
expectCityNotMatchStrategy,
|
||
blockJobNotSuit,
|
||
blockBossNotActive,
|
||
blockBossNotNewChat
|
||
})
|
||
await toRecommendPage(hooks)
|
||
// goto search
|
||
|
||
// ;await browser.close()
|
||
} catch (err) {
|
||
closeBrowserWindow()
|
||
throw err
|
||
}
|
||
}
|
||
|
||
export async function closeBrowserWindow () {
|
||
browser?.close()
|
||
const browserProcess = browser?.process()
|
||
if (browserProcess) {
|
||
try {
|
||
process.kill(browserProcess.pid)
|
||
}
|
||
catch {}
|
||
}
|
||
browser = null
|
||
page = null
|
||
}
|
||
|
||
async function storeStorage (page) {
|
||
const [
|
||
cookies, localStorage
|
||
] = await Promise.all([
|
||
page.cookies(),
|
||
page.evaluate(() => {
|
||
return JSON.stringify(window.localStorage)
|
||
}).then(res => JSON.parse(res))
|
||
])
|
||
return Promise.all(
|
||
[
|
||
writeStorageFile('boss-cookies.json', cookies),
|
||
writeStorageFile('boss-local-storage.json', localStorage),
|
||
]
|
||
)
|
||
}
|