mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-07 16:37:06 +08:00
1164 lines
49 KiB
JavaScript
1164 lines
49 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 } 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 } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||
import { activeDescList } from './constant.mjs'
|
||
import { parseSalary } from "@geekgeekrun/sqlite-plugin/dist/utils/parser"
|
||
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
|
||
export async function initPuppeteer () {
|
||
// production
|
||
const importResult = await Promise.all(
|
||
[
|
||
import('puppeteer-extra'),
|
||
import('puppeteer-extra-plugin-stealth')
|
||
]
|
||
)
|
||
puppeteer = importResult[0].default
|
||
StealthPlugin = importResult[1].default
|
||
puppeteer.use(StealthPlugin())
|
||
|
||
return {
|
||
puppeteer,
|
||
StealthPlugin
|
||
}
|
||
}
|
||
|
||
const bossCookies = readStorageFile('boss-cookies.json')
|
||
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||
|
||
const targetCompanyList = readConfigFile('target-company-list.json').filter(it => !!it.trim());
|
||
|
||
const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter
|
||
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 = readConfigFile('boss.json').expectCityList ?? []
|
||
|
||
const strategyScopeOptionWhenMarkJobCityNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkJobCityNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
|
||
|
||
// salary
|
||
const expectSalaryLow = parseFloat(readConfigFile('boss.json').expectSalaryLow) || null
|
||
const expectSalaryHigh = parseFloat(readConfigFile('boss.json').expectSalaryHigh) || null
|
||
const expectSalaryCalculateWay = readConfigFile('boss.json').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
|
||
const expectWorkExpList = readConfigFile('boss.json').expectWorkExpList ?? []
|
||
const expectWorkExpNotMatchStrategy = readConfigFile('boss.json').expectWorkExpNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
|
||
const strategyScopeOptionWhenMarkJobWorkExpNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkJobWorkExpNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
|
||
|
||
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,
|
||
} = readConfigFile('boss.json')
|
||
if (
|
||
expectJobRegExpStr &&
|
||
!expectJobNameRegExpStr &&
|
||
!expectJobTypeRegExpStr &&
|
||
!expectJobDescRegExpStr
|
||
) {
|
||
expectJobNameRegExpStr = expectJobRegExpStr
|
||
expectJobTypeRegExpStr = expectJobRegExpStr
|
||
expectJobDescRegExpStr = expectJobRegExpStr
|
||
}
|
||
|
||
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)
|
||
|
||
/**
|
||
* @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.v-transfer-dom')
|
||
return alls?.[alls.length - 1]
|
||
})()
|
||
let isOptionChosen = false
|
||
if (chooseReasonDialogProxy) {
|
||
switch (reasonCode) {
|
||
case MarkAsNotSuitReason.BOSS_INACTIVE: {
|
||
const bossNotActiveOptionProxy = await chooseReasonDialogProxy.$(`.zp-type-item[title="BOSS活跃度低"]`)
|
||
if (bossNotActiveOptionProxy) {
|
||
await bossNotActiveOptionProxy.click()
|
||
isOptionChosen = true
|
||
} else {
|
||
const recruitStoppedOptionProxy = await chooseReasonDialogProxy.$(`.zp-type-item[title="职位停招/招满"]`)
|
||
if (recruitStoppedOptionProxy) {
|
||
await recruitStoppedOptionProxy.click()
|
||
isOptionChosen = true
|
||
}
|
||
}
|
||
break
|
||
}
|
||
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
|
||
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT:
|
||
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="面试过/入职过"]`))
|
||
if (opProxy) {
|
||
await opProxy.click()
|
||
isOptionChosen = true
|
||
}
|
||
break
|
||
}
|
||
case MarkAsNotSuitReason.JOB_NOT_SUIT:
|
||
default: {
|
||
const jobNotSuitOptionProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="职位"]`)) ?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
|
||
if (jobNotSuitOptionProxy) {
|
||
await jobNotSuitOptionProxy.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) {
|
||
let isJobNameSuit = true
|
||
try {
|
||
if (expectJobNameRegExpStr.trim()) {
|
||
const regExp = new RegExp(expectJobNameRegExpStr, 'i')
|
||
isJobNameSuit = regExp.test(jobInfo.jobName)
|
||
}
|
||
} catch {
|
||
}
|
||
let isJobTypeSuit = true
|
||
try {
|
||
if (expectJobTypeRegExpStr.trim()) {
|
||
const regExp = new RegExp(expectJobTypeRegExpStr, 'i')
|
||
isJobTypeSuit = regExp.test(jobInfo.positionName)
|
||
}
|
||
} catch {
|
||
}
|
||
let isJobDescSuit = true
|
||
try {
|
||
if (expectJobDescRegExpStr.trim()) {
|
||
const regExp = new RegExp(expectJobDescRegExpStr, 'i')
|
||
isJobDescSuit = regExp.test(jobInfo.postDescription)
|
||
}
|
||
} catch {
|
||
}
|
||
return isJobNameSuit && isJobTypeSuit && isJobDescSuit
|
||
}
|
||
|
||
async function setFilterCondition (selectedFilters) {
|
||
const {
|
||
salaryList = [],
|
||
experienceList = [],
|
||
degreeList = [],
|
||
scaleList = [],
|
||
industryList = []
|
||
} = selectedFilters
|
||
|
||
const placeholderTexts = ['薪资待遇', '工作经验', '学历要求', '公司行业', '公司规模']
|
||
const optionKaPrefixes = ['sel-job-rec-salary-', 'sel-job-rec-exp-', 'sel-job-rec-degree-', 'sel-industry-', 'sel-job-rec-scale-']
|
||
const conditionArr = [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) => {
|
||
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 (!filterDropdownCssList.includes('is-select') && !currentFilterConditions.length) {
|
||
continue
|
||
} else {
|
||
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)
|
||
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
|
||
}).then((res) => {
|
||
return res.json()
|
||
})
|
||
await Promise.all([
|
||
page.goto(recommendJobPageUrl, { timeout: 120 * 1000 }),
|
||
page.waitForNavigation(),
|
||
])
|
||
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")
|
||
}
|
||
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)
|
||
}
|
||
|
||
// check set security question tip modal
|
||
let setSecurityQuestionTipModelProxy = await page.$('.dialog-wrap.dialog-account-safe')
|
||
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 INIT_START_EXCEPT_JOB_INDEX = 0
|
||
let currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||
afterPageLoad: while (true) {
|
||
let expectJobList
|
||
iterateFilterCondition: for (
|
||
const filterCondition of combineFiltersWithConstraintsGenerator(
|
||
anyCombineRecommendJobFilter
|
||
)
|
||
) {
|
||
findInCurrentFilterCondition: while(true) {
|
||
await sleepWithRandomDelay(2500)
|
||
|
||
await Promise.all([
|
||
page.waitForSelector('.c-expect-select .expect-list .expect-item'),
|
||
page.waitForSelector('.job-list-container .rec-job-list')
|
||
])
|
||
await page.click(`.c-expect-select .expect-list .expect-item`)
|
||
const currentActiveJobIndex = await page.evaluate(`
|
||
[...document.querySelectorAll('.c-expect-select .expect-list .expect-item')].findIndex(it => it.classList.contains('active'))
|
||
`)
|
||
|
||
expectJobList = await page.evaluate(`document.querySelector('.c-expect-select')?.__vue__?.expectList`)
|
||
if (currentActiveJobIndex === currentExceptJobIndex) {
|
||
// first navigation and can immediately start chat (recommend job)
|
||
} else {
|
||
// not first navigation and should choose a job (except job)
|
||
// click first expect job
|
||
const expectJobTabHandlers = await page.$$('.c-expect-select .expect-list .expect-item')
|
||
await expectJobTabHandlers[currentExceptJobIndex].click()
|
||
await page.waitForResponse(
|
||
response => {
|
||
if (
|
||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json')
|
||
) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
);
|
||
await storeStorage(page).catch(() => void 0)
|
||
await sleepWithRandomDelay(2000)
|
||
}
|
||
await sleepWithRandomDelay(3000)
|
||
await setFilterCondition(filterCondition)
|
||
|
||
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')) {
|
||
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
|
||
const recommendJobListElProxy = await page.$('.job-list-container .rec-job-list')
|
||
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) {
|
||
if (expectSalaryHigh && salaryData.high > expectSalaryHigh) {
|
||
return false
|
||
}
|
||
if (expectSalaryLow && salaryData.low < expectSalaryLow) {
|
||
return false
|
||
}
|
||
} else if (expectSalaryCalculateWay === SalaryCalculateWay.ANNUAL_PACKAGE) {
|
||
const salaryDataMonth = salaryData.month || 12
|
||
if (expectSalaryHigh && (salaryData.high * salaryDataMonth) / 10 > expectSalaryHigh) {
|
||
return false
|
||
}
|
||
if (expectSalaryLow && (salaryData.low * salaryDataMonth) / 10 < expectSalaryLow) {
|
||
return false
|
||
}
|
||
}
|
||
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
|
||
)
|
||
)
|
||
})
|
||
}
|
||
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 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)
|
||
}
|
||
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 active() {
|
||
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
|
||
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
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
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||
try {
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||
extInfo: null,
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
},
|
||
async city() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
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
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||
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
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
},
|
||
async workExp() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
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
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||
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
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
},
|
||
async jobDetail() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
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
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||
try {
|
||
await hooks.jobMarkedAsNotSuit.promise(
|
||
targetJobData,
|
||
{
|
||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||
extInfo: null,
|
||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
},
|
||
async salary() {
|
||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||
if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||
try {
|
||
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
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
else if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||
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
|
||
}
|
||
)
|
||
} catch {
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
//#region
|
||
// null
|
||
// 刚刚活跃 // 今日活跃 // 昨日活跃 // 3日内活跃 // 本周活跃 // 2周内活跃
|
||
// 本月活跃 // 2月内活跃 // 3月内活跃 // 4月内活跃 // 5月内活跃 // 近半年活跃 // 半年前活跃
|
||
//#endregion
|
||
const indexOfActiveText = activeDescList.indexOf(targetJobData.bossInfo.activeTimeDesc)
|
||
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)
|
||
) {
|
||
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[markOnBossCondition]()
|
||
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 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 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
|
||
}
|
||
);
|
||
const res = await addFriendResponse.json()
|
||
|
||
if (res.code !== 0) {
|
||
// startup chat error, may the chance of today has used out
|
||
if (res.zpData.bizCode === 1 && res.zpData.bizData?.chatRemindDialog?.blockLevel === 0 && res.zpData.bizData?.chatRemindDialog?.content === `今日沟通人数已达上限,请明天再试`) {
|
||
await storeStorage(page).catch(() => void 0)
|
||
throw new Error('STARTUP_CHAT_ERROR_DUE_TO_TODAY_CHANCE_HAS_USED_OUT')
|
||
} else {
|
||
console.error(res)
|
||
throw new Error('STARTUP_CHAT_ERROR_WITH_UNKNOWN_ERROR')
|
||
}
|
||
} else {
|
||
await hooks.newChatStartup?.promise(targetJobData, { chatStartupFrom: ChatStartupFrom.AutoFromRecommendList })
|
||
blockBossNotNewChat.add(targetJobData.jobInfo.encryptUserId)
|
||
|
||
await storeStorage(page).catch(() => void 0)
|
||
await sleepWithRandomDelay(1500)
|
||
const closeDialogButtonProxy = await page.$('.greet-boss-dialog .greet-boss-footer .cancel-btn')
|
||
await closeDialogButtonProxy.click()
|
||
await sleepWithRandomDelay(2000)
|
||
}
|
||
// #endregion
|
||
} 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 (
|
||
currentExceptJobIndex + 1 >= expectJobList.length
|
||
) {
|
||
hooks.noPositionFoundForCurrentJob?.call()
|
||
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
||
await sleep((20 + 30 * Math.random()) * 1000)
|
||
await Promise.all([
|
||
page.reload(),
|
||
page.waitForNavigation()
|
||
])
|
||
currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||
} else {
|
||
hooks.noPositionFoundForCurrentJob?.call()
|
||
await sleep((10 + 15 * Math.random()) * 1000)
|
||
currentExceptJobIndex += 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()
|
||
page = (await browser.pages())[0]
|
||
//set cookies
|
||
hooks.cookieWillSet?.call(bossCookies)
|
||
for(let i = 0; i < bossCookies.length; i++){
|
||
await page.setCookie(bossCookies[i]);
|
||
}
|
||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
|
||
await page.bringToFront()
|
||
await hooks.mainFlowWillLaunch?.callAsync({
|
||
jobNotMatchStrategy,
|
||
jobNotActiveStrategy,
|
||
expectCityNotMatchStrategy,
|
||
blockJobNotSuit,
|
||
blockBossNotActive,
|
||
})
|
||
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) {
|
||
process.kill(browserProcess.pid)
|
||
}
|
||
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),
|
||
]
|
||
)
|
||
}
|