diff --git a/packages/geek-auto-start-chat-with-boss/combineCalculator.mjs b/packages/geek-auto-start-chat-with-boss/combineCalculator.mjs index 84a637e..726066a 100644 --- a/packages/geek-auto-start-chat-with-boss/combineCalculator.mjs +++ b/packages/geek-auto-start-chat-with-boss/combineCalculator.mjs @@ -90,7 +90,7 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) { //#region get count of combinations // 计算符合限制条件的组合数量 -export function calculateTotalCombinations(selectedFilters) { +export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) { const { salaryList = [], experienceList = [], @@ -106,8 +106,21 @@ export function calculateTotalCombinations(selectedFilters) { const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多 const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个 - return [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => { + let result = [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => { return accu * cur.length }, 1) + if (!includeEmptyCondition) { + result -= 1 + } + return result } //#endregion + +export function checkAnyCombineBossRecommendFilterHasCondition(value) { + if (!Object.keys(value ?? {}).length) { + return false + } + return Object.keys(value).some((k) => { + return !!value[k]?.length + }) +} diff --git a/packages/geek-auto-start-chat-with-boss/constant.mjs b/packages/geek-auto-start-chat-with-boss/constant.mjs index 0e1662f..f7e54d6 100644 --- a/packages/geek-auto-start-chat-with-boss/constant.mjs +++ b/packages/geek-auto-start-chat-with-boss/constant.mjs @@ -1,3 +1,7 @@ export const activeDescList = [ '', "半年前活跃","近半年活跃","5月内活跃","4月内活跃","3月内活跃","2月内活跃","本月活跃","2周内活跃","本周活跃","3日内活跃","昨日活跃","今日活跃","刚刚活跃" -] \ No newline at end of file +] + +export const RECOMMEND_JOB_ENTRY_SELECTOR = `.c-expect-select a[ka="jobs_recommend_tab_click"]` +export const USER_SET_EXPECT_JOB_ENTRIES_SELECTOR = `.c-expect-select .expect-list .expect-item` +export const SEARCH_BOX_SELECTOR = `.c-search-input .search-input-box` \ No newline at end of file diff --git a/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json b/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json index 766a66c..86602cd 100644 --- a/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json +++ b/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json @@ -6,6 +6,7 @@ "scaleList": [], "industryList": [] }, + "isSkipEmptyConditionForCombineRecommendJobFilter": false, "expectJobRegExpStr": "", "jobNotMatchStrategy": 1, "jobNotActiveStrategy": 1, @@ -13,6 +14,7 @@ "expectCityList": [], "expectCityNotMatchStrategy": 3, "strategyScopeOptionWhenMarkJobCityNotMatch": 2, + "jobDetailRegExpMatchLogic": 1, "autoReminder": { "throttleIntervalMinutes": 10, "rechatLimitDay": 21, @@ -21,5 +23,11 @@ "recentMessageQuantityForLlm": 8, "rechatLlmFallback": 1, "onlyRemindBossWithExpectJobType": true - } + }, + "jobSourceList": [ + { + "type": "expect", + "enabled": true + } + ] } \ No newline at end of file diff --git a/packages/geek-auto-start-chat-with-boss/index.mjs b/packages/geek-auto-start-chat-with-boss/index.mjs index f433826..4324203 100644 --- a/packages/geek-auto-start-chat-with-boss/index.mjs +++ b/packages/geek-auto-start-chat-with-boss/index.mjs @@ -12,12 +12,21 @@ 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 { + calculateTotalCombinations, + combineFiltersWithConstraintsGenerator, + checkAnyCombineBossRecommendFilterHasCondition +} 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 { MarkAsNotSuitReason, MarkAsNotSuitOp, StrategyScopeOptionWhenMarkJobNotMatch, SalaryCalculateWay, JobDetailRegExpMatchLogic, JobSource } 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" const jobFilterConditionsMapByCode = {} Object.values(jobFilterConditions).forEach(arr => { @@ -78,6 +87,10 @@ const bossLocalStorage = readStorageFile('boss-local-storage.json') const targetCompanyList = readConfigFile('target-company-list.json').filter(it => !!it.trim()); const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter +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 @@ -99,6 +112,8 @@ 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 +let jobDetailRegExpMatchLogic = readConfigFile('boss.json').jobDetailRegExpMatchLogic ?? JobDetailRegExpMatchLogic.EVERY + const markAsNotActiveSelectedTimeRange = (() => { let n = readConfigFile('boss.json').markAsNotActiveSelectedTimeRange if ( @@ -132,6 +147,60 @@ if ( 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` @@ -262,8 +331,8 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) { return result } -export function testIfJobTitleOrDescriptionSuit (jobInfo) { - let isJobNameSuit = true +export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) { + let isJobNameSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true try { if (expectJobNameRegExpStr.trim()) { const regExp = new RegExp(expectJobNameRegExpStr, 'i') @@ -271,7 +340,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) { } } catch { } - let isJobTypeSuit = true + let isJobTypeSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true try { if (expectJobTypeRegExpStr.trim()) { const regExp = new RegExp(expectJobTypeRegExpStr, 'i') @@ -279,7 +348,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) { } } catch { } - let isJobDescSuit = true + let isJobDescSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true try { if (expectJobDescRegExpStr.trim()) { const regExp = new RegExp(expectJobDescRegExpStr, 'i') @@ -287,7 +356,12 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) { } } catch { } - return isJobNameSuit && isJobTypeSuit && isJobDescSuit + if (matchLogic === JobDetailRegExpMatchLogic.SOME) { + return isJobNameSuit || isJobTypeSuit || isJobDescSuit + } + else { + return isJobNameSuit && isJobTypeSuit && isJobDescSuit + } } async function setFilterCondition (selectedFilters) { @@ -465,8 +539,94 @@ async function toRecommendPage (hooks) { } } - const INIT_START_EXCEPT_JOB_INDEX = 0 - let currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX + 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) { let expectJobList iterateFilterCondition: for ( @@ -478,26 +638,44 @@ async function toRecommendPage (hooks) { await sleepWithRandomDelay(2500) await Promise.all([ - page.waitForSelector('.c-expect-select .expect-list .expect-item'), - page.waitForSelector('.job-list-container .rec-job-list') + 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(`.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')) - `) + // 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 ( + 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 (currentActiveJobIndex === currentExceptJobIndex) { + if (onPageCurrentSourceIndex === currentSourceIndex) { // 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 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/pc/recommend/job/list.json') || + response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/search/joblist.json') ) { return true } @@ -507,9 +685,12 @@ async function toRecommendPage (hooks) { await storeStorage(page).catch(() => void 0) await sleepWithRandomDelay(2000) } - await sleepWithRandomDelay(3000) + 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 { @@ -517,7 +698,10 @@ async function toRecommendPage (hooks) { page.on( 'request', function reqHandler (request) { - if (request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json')) { + 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) => { @@ -540,9 +724,17 @@ async function toRecommendPage (hooks) { } } ) - // job list - const recommendJobListElProxy = await page.$('.job-list-container .rec-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`) @@ -739,7 +931,22 @@ async function toRecommendPage (hooks) { const notSuitConditionHandleMap = { async active() { blockBossNotActive.add(targetJobData.jobInfo.encryptUserId) - if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) { + 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 { const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.BOSS_INACTIVE) await hooks.jobMarkedAsNotSuit.promise( @@ -751,21 +958,8 @@ async function toRecommendPage (hooks) { 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 + markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS, + jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) } catch { @@ -774,7 +968,22 @@ async function toRecommendPage (hooks) { }, async city() { blockJobNotSuit.add(targetJobData.jobInfo.encryptId) - if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) { + 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 { const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_CITY_NOT_SUIT) await hooks.jobMarkedAsNotSuit.promise( @@ -785,21 +994,8 @@ async function toRecommendPage (hooks) { 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 + markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS, + jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) } catch { @@ -808,7 +1004,22 @@ async function toRecommendPage (hooks) { }, async workExp() { blockJobNotSuit.add(targetJobData.jobInfo.encryptId) - if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) { + 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 { const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT) await hooks.jobMarkedAsNotSuit.promise( @@ -819,21 +1030,8 @@ async function toRecommendPage (hooks) { 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 + markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS, + jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) } catch { @@ -842,7 +1040,22 @@ async function toRecommendPage (hooks) { }, async jobDetail() { blockJobNotSuit.add(targetJobData.jobInfo.encryptId) - if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) { + 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 { const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_NOT_SUIT) await hooks.jobMarkedAsNotSuit.promise( @@ -854,21 +1067,8 @@ async function toRecommendPage (hooks) { 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 + markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS, + jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) } catch { @@ -877,7 +1077,24 @@ async function toRecommendPage (hooks) { }, async salary() { blockJobNotSuit.add(targetJobData.jobInfo.encryptId) - if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) { + 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 { const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT) await hooks.jobMarkedAsNotSuit.promise( @@ -889,23 +1106,8 @@ async function toRecommendPage (hooks) { 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 + markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS, + jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) } catch { @@ -938,7 +1140,7 @@ async function toRecommendPage (hooks) { notSuitReasonIdToStrategyMap.workExp = expectWorkExpNotMatchStrategy } if ( - !testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo) + !testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo, jobDetailRegExpMatchLogic) ) { notSuitReasonIdToStrategyMap.jobDetail = jobNotMatchStrategy } @@ -960,7 +1162,7 @@ async function toRecommendPage (hooks) { // 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]() + await notSuitConditionHandleMap[markOnLocalDbCondition]() continue continueFind } // #endregion @@ -1035,7 +1237,13 @@ async function toRecommendPage (hooks) { throw new Error('STARTUP_CHAT_ERROR_WITH_UNKNOWN_ERROR') } } else { - await hooks.newChatStartup?.promise(targetJobData, { chatStartupFrom: ChatStartupFrom.AutoFromRecommendList }) + await hooks.newChatStartup?.promise( + targetJobData, + { + chatStartupFrom: ChatStartupFrom.AutoFromRecommendList, + jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] + } + ) blockBossNotNewChat.add(targetJobData.jobInfo.encryptUserId) await storeStorage(page).catch(() => void 0) @@ -1078,20 +1286,20 @@ async function toRecommendPage (hooks) { } // for of reach terminal if ( - currentExceptJobIndex + 1 >= expectJobList.length + currentSourceIndex + 1 >= computedSourceList.length ) { hooks.noPositionFoundForCurrentJob?.call() hooks.noPositionFoundAfterTraverseAllJob?.call() await sleep((20 + 30 * Math.random()) * 1000) await Promise.all([ - page.reload(), + page.goto(`https://www.zhipin.com/web/geek/jobs`), page.waitForNavigation() ]) - currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX + currentSourceIndex = 0 } else { hooks.noPositionFoundForCurrentJob?.call() await sleep((10 + 15 * Math.random()) * 1000) - currentExceptJobIndex += 1 + currentSourceIndex += 1 } } } @@ -1118,12 +1326,13 @@ export async function mainLoop (hooks) { } await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage) await page.bringToFront() - await hooks.mainFlowWillLaunch?.callAsync({ + await hooks.mainFlowWillLaunch?.promise({ jobNotMatchStrategy, jobNotActiveStrategy, expectCityNotMatchStrategy, blockJobNotSuit, blockBossNotActive, + blockBossNotNewChat }) await toRecommendPage(hooks) // goto search diff --git a/packages/run-core-of-geek-auto-start-chat-with-boss/main.mjs b/packages/run-core-of-geek-auto-start-chat-with-boss/main.mjs index 473e0a4..7945286 100644 --- a/packages/run-core-of-geek-auto-start-chat-with-boss/main.mjs +++ b/packages/run-core-of-geek-auto-start-chat-with-boss/main.mjs @@ -56,7 +56,8 @@ const main = async () => { newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']), noPositionFoundForCurrentJob: new SyncHook(), noPositionFoundAfterTraverseAllJob: new SyncHook(), - errorEncounter: new SyncHook(['errorInfo']) + errorEncounter: new SyncHook(['errorInfo']), + encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']) } initPlugins(hooks) await hooks.daemonInitialized.callAsync() diff --git a/packages/sqlite-plugin/src/entity/ChatStartupLog.ts b/packages/sqlite-plugin/src/entity/ChatStartupLog.ts index 94ff5aa..a8d2481 100644 --- a/packages/sqlite-plugin/src/entity/ChatStartupLog.ts +++ b/packages/sqlite-plugin/src/entity/ChatStartupLog.ts @@ -1,3 +1,4 @@ +import { JobSource } from "../enums"; import { requireTypeorm } from "../utils/module-loader"; const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm() @@ -29,4 +30,9 @@ export class ChatStartupLog { nullable: true }) autoStartupChatRecordId?: number; + + @Column({ + nullable: true + }) + jobSource?: JobSource; } diff --git a/packages/sqlite-plugin/src/entity/MarkAsNotSuitLog.ts b/packages/sqlite-plugin/src/entity/MarkAsNotSuitLog.ts index 163035c..c3325fc 100644 --- a/packages/sqlite-plugin/src/entity/MarkAsNotSuitLog.ts +++ b/packages/sqlite-plugin/src/entity/MarkAsNotSuitLog.ts @@ -1,4 +1,4 @@ -import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums"; +import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums"; import { requireTypeorm } from "../utils/module-loader"; import { ChatStartupFrom } from "./ChatStartupLog"; const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm() @@ -41,4 +41,9 @@ export class MarkAsNotSuitLog { nullable: true }) autoStartupChatRecordId?: number; + + @Column({ + nullable: true + }) + jobSource?: JobSource; } diff --git a/packages/sqlite-plugin/src/enums.ts b/packages/sqlite-plugin/src/enums.ts index ddab6c5..ca74232 100644 --- a/packages/sqlite-plugin/src/enums.ts +++ b/packages/sqlite-plugin/src/enums.ts @@ -22,4 +22,15 @@ export enum StrategyScopeOptionWhenMarkJobNotMatch { export enum SalaryCalculateWay { MONTH_SALARY = 1, ANNUAL_PACKAGE = 2, -} \ No newline at end of file +} + +export enum JobDetailRegExpMatchLogic { + EVERY = 1, + SOME = 2, +} + +export enum JobSource { + expect = 1, + recommend = 2, + search = 3, +} diff --git a/packages/sqlite-plugin/src/handlers.ts b/packages/sqlite-plugin/src/handlers.ts index fcda892..61bc121 100644 --- a/packages/sqlite-plugin/src/handlers.ts +++ b/packages/sqlite-plugin/src/handlers.ts @@ -245,7 +245,7 @@ export async function saveChatStartupRecord( ds: DataSource, _jobInfo, { encryptUserId }, - { autoStartupChatRecordId = undefined, chatStartupFrom = undefined } = {} + { autoStartupChatRecordId = undefined, chatStartupFrom = undefined, jobSource = undefined } = {} ) { const { jobInfo } = _jobInfo; @@ -256,7 +256,8 @@ export async function saveChatStartupRecord( encryptCurrentUserId: encryptUserId, encryptJobId: jobInfo.encryptId, autoStartupChatRecordId, - chatStartupFrom + chatStartupFrom, + jobSource, } Object.assign(chatStartupLog, chatStartupLogPayload) @@ -270,7 +271,7 @@ export async function saveMarkAsNotSuitRecord( ds: DataSource, _jobInfo, { encryptUserId }, - { autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined } = {} + { autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined, jobSource = undefined } = {} ) { const { jobInfo } = _jobInfo; @@ -284,7 +285,8 @@ export async function saveMarkAsNotSuitRecord( markFrom, markReason, extInfo: extInfo ? JSON.stringify(extInfo) : undefined, - markOp + markOp, + jobSource, } Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload) @@ -333,9 +335,33 @@ export async function getNotSuitMarkRecordsInLastSomeDays (ds: DataSource, days const result = await repo.findBy({ date: Raw(alias => `DATE(${alias}) >= DATE('${ new Date( - Number(new Date()) - 7 * 24 * 60 * 60 * 1000 + Number(new Date()) - days * 24 * 60 * 60 * 1000 ).toISOString() }')`) }) return result } + +export async function getChatStartupRecordsInLastSomeDays (ds: DataSource, days = 0) { + const repo = ds.getRepository(ChatStartupLog) + const result = await repo.findBy({ + date: Raw(alias => `DATE(${alias}) >= DATE('${ + new Date( + Number(new Date()) - days * 24 * 60 * 60 * 1000 + ).toISOString() + }')`) + }) + return result +} + +export async function getBossIdsByJobIds (ds: DataSource, jobIds: string[] = []) { + const repo = ds.getRepository(JobInfo) + const result = await repo.find({ + where: jobIds.map( + id => ({ + encryptJobId: id + }) + ) + }) + return result +} diff --git a/packages/sqlite-plugin/src/index.ts b/packages/sqlite-plugin/src/index.ts index 3fdd3e4..e4f6959 100644 --- a/packages/sqlite-plugin/src/index.ts +++ b/packages/sqlite-plugin/src/index.ts @@ -23,13 +23,21 @@ import { ChatMessageRecord } from './entity/ChatMessageRecord' import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord' import sqlite3 from 'sqlite3'; -import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord, getNotSuitMarkRecordsInLastSomeDays } from "./handlers"; +import { + saveChatStartupRecord, + saveJobInfoFromRecommendPage, + saveMarkAsNotSuitRecord, + getNotSuitMarkRecordsInLastSomeDays, + getChatStartupRecordsInLastSomeDays, + getBossIdsByJobIds +} from "./handlers"; import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable"; import minimist from 'minimist' import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable"; import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums"; import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog"; import { Init1000000000000 } from "./migrations/1000000000000-Init"; +import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog"; export function initDb(dbFilePath) { const { DataSource } = requireTypeorm() @@ -65,6 +73,7 @@ export function initDb(dbFilePath) { UpdateChatStartupLogTable1729182577167, UpdateBossInfoTable1732032381304, AddColumnForMarkAsNotSuitLog1746092370665, + AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 ], migrationsRun: true }); @@ -111,6 +120,7 @@ export default class SqlitePlugin { expectCityNotMatchStrategy, blockJobNotSuit, blockBossNotActive, + blockBossNotNewChat }) => { if ( jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || @@ -118,8 +128,11 @@ export default class SqlitePlugin { expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ) { const ds = await this.initPromise; - const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7) ?? []); - if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) { + const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7)) ?? []; + if ( + jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || + jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS + ) { last7DayMarkRecords .filter(it => [ @@ -134,7 +147,10 @@ export default class SqlitePlugin { id => blockJobNotSuit.add(id) ) } - if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) { + if ( + jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || + jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS + ) { last7DayMarkRecords .filter(it => it.markReason === MarkAsNotSuitReason.BOSS_INACTIVE) .map( @@ -144,7 +160,10 @@ export default class SqlitePlugin { id => blockJobNotSuit.add(id) ) } - if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) { + if ( + expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || + expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS + ) { last7DayMarkRecords .filter(it => it.markReason === MarkAsNotSuitReason.JOB_CITY_NOT_SUIT) .map( @@ -154,6 +173,12 @@ export default class SqlitePlugin { id => blockJobNotSuit.add(id) ) } + const last30DayChatStartupRecords = (await getChatStartupRecordsInLastSomeDays(ds, 30)) ?? []; + const chattedJobIds = last30DayChatStartupRecords.map(it => it.encryptJobId) + const chattedBossIds = ((await getBossIdsByJobIds(ds, chattedJobIds)) ?? []).map(it => it.encryptBossId) + for (const id of chattedBossIds) { + blockBossNotNewChat.add(id) + } } } ); @@ -163,22 +188,24 @@ export default class SqlitePlugin { await saveJobInfoFromRecommendPage(ds, _jobInfo); }); - hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList } = {}) => { + hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList, jobSource = undefined } = {}) => { const ds = await this.initPromise; return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, { autoStartupChatRecordId: this.runRecordId, - chatStartupFrom + chatStartupFrom, + jobSource }); }); - hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined } = {}) => { + hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined, jobSource = undefined } = {}) => { const ds = await this.initPromise; return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, { autoStartupChatRecordId: this.runRecordId, markFrom, markReason, extInfo, - markOp + markOp, + jobSource }); }); } diff --git a/packages/sqlite-plugin/src/migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog.ts b/packages/sqlite-plugin/src/migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog.ts new file mode 100644 index 0000000..194af72 --- /dev/null +++ b/packages/sqlite-plugin/src/migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog.ts @@ -0,0 +1,64 @@ +import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm" +import { VBossLibrary } from "../entity/VBossLibrary"; +import { VChatStartupLog } from "../entity/VChatStartupLog"; +import { VCompanyLibrary } from "../entity/VCompanyLibrary"; +import { VJobLibrary } from "../entity/VJobLibrary"; +import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog"; +import { JobSource } from "../enums"; + +const ViewEntities = [ + VBossLibrary, + VChatStartupLog, + VCompanyLibrary, + VJobLibrary, + VMarkAsNotSuitLog, +] +export class AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + for (const EntityDefinition of ViewEntities) { + const dataSource = queryRunner.connection as DataSource; + const viewMetadata = dataSource.getMetadata(EntityDefinition); + await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`); + } + if (await queryRunner.hasTable("mark_as_not_suit_log")) { + if (!await queryRunner.hasColumn("mark_as_not_suit_log", "jobSource")) { + await queryRunner.addColumn( + "mark_as_not_suit_log", + new TableColumn({ + name: "jobSource", + type: "number", + isNullable: true, + }) + ); + await queryRunner.query(`UPDATE mark_as_not_suit_log SET jobSource=?`, [JobSource.expect]); + } + } + if (await queryRunner.hasTable("chat_startup_log")) { + if (!await queryRunner.hasColumn("chat_startup_log", "jobSource")) { + await queryRunner.addColumn( + "chat_startup_log", + new TableColumn({ + name: "jobSource", + type: "number", + isNullable: true, + }) + ); + await queryRunner.query(`UPDATE chat_startup_log SET jobSource=?`, [JobSource.expect]); + } + } + for (const EntityDefinition of ViewEntities) { + const dataSource = queryRunner.connection as DataSource; + const viewMetadata = dataSource.getMetadata(EntityDefinition); + let expression = viewMetadata.expression; + if (typeof expression === 'function') { + expression = expression(dataSource).getQuery(); + } + await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`); + } + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/packages/ui/package.json b/packages/ui/package.json index b76d3f1..5d75824 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -44,7 +44,8 @@ "pinia": "^3.0.2", "puppeteer": "20.1.0", "puppeteer-extra-plugin-stealth": "2.11.2", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "vuedraggable": "^4.1.0" }, "devDependencies": { "@electron-toolkit/eslint-config": "^1.0.2", diff --git a/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index.ts b/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index.ts index 0ec7ff4..5108946 100644 --- a/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index.ts +++ b/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index.ts @@ -93,7 +93,8 @@ const runAutoChat = async () => { jobMarkedAsNotSuit: new AsyncSeriesHook(['positionInfoDetail', 'markDetail']), noPositionFoundForCurrentJob: new SyncHook(), noPositionFoundAfterTraverseAllJob: new SyncHook(), - errorEncounter: new SyncHook(['errorInfo']) + errorEncounter: new SyncHook(['errorInfo']), + encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']) } initPlugins(hooks) diff --git a/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts b/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts index 38895f2..eed4c86 100644 --- a/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts +++ b/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts @@ -4,6 +4,10 @@ import { readStorageFile, writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' +import { + RECOMMEND_JOB_ENTRY_SELECTOR, + USER_SET_EXPECT_JOB_ENTRIES_SELECTOR, +} from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs' import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs' import { saveJobInfoFromRecommendPage, @@ -13,7 +17,7 @@ import { } from '@geekgeekrun/sqlite-plugin/dist/handlers' import { initDb } from '@geekgeekrun/sqlite-plugin' import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' -import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums' +import { MarkAsNotSuitReason, JobSource } from '@geekgeekrun/sqlite-plugin/dist/enums' import fs from 'node:fs' import { Target } from 'puppeteer' @@ -38,6 +42,49 @@ const attachRequestsListener = async (target: Target) => { if (!page) { return } + async function getCurrentJobSource() { + const methodMap = { + async recommend() { + return await page.evaluate( + ({ RECOMMEND_JOB_ENTRY_SELECTOR }) => { + return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active') + }, + { + RECOMMEND_JOB_ENTRY_SELECTOR + } + ) + }, + async expect() { + return await page.evaluate( + ({ USER_SET_EXPECT_JOB_ENTRIES_SELECTOR }) => { + return [...document.querySelectorAll(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)].some((el) => + el.classList.contains('active') + ) + }, + { + USER_SET_EXPECT_JOB_ENTRIES_SELECTOR + } + ) + }, + async search() { + const elHandle = await page.$(`.page-jobs-main`) + const currentKeyWord = await elHandle?.evaluate((el) => { + return el?.__vue__?.formData?.query + }) + return !!currentKeyWord + } + } + for (const [type, func] of Object.entries(methodMap)) { + try { + if (await func()) { + return type + } + } catch (err) { + console.error('encounter error when get job source') + } + } + return null + } page.on('response', async (response) => { if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) { @@ -81,7 +128,7 @@ const attachRequestsListener = async (target: Target) => { const reasonCodeToTextMap = await readStorageFile( 'job-not-suit-reason-code-to-text-cache.json' ) - + const jobSource = await getCurrentJobSource() const markDetail = { markFrom: ChatStartupFrom.ManuallyFromRecommendList, extInfo: { @@ -90,12 +137,14 @@ const attachRequestsListener = async (target: Target) => { text: reasonCodeToTextMap[chosenCode] } }, - markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON + markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON, + jobSource: JobSource[jobSource] } gtag('job_marked_as_not_suit', { markFrom: markDetail.markFrom, bossActiveTimeDesc: currentJobData?.bossInfo?.activeTimeDesc, - encryptJobId: currentJobData?.jobInfo?.encryptId + encryptJobId: currentJobData?.jobInfo?.encryptId, + jobSource: JobSource[jobSource] }) if (reasonCodeToTextMap[chosenCode]?.includes('活跃度低')) { markDetail.markReason = MarkAsNotSuitReason.BOSS_INACTIVE @@ -131,9 +180,11 @@ const attachRequestsListener = async (target: Target) => { const currentUserInfo = await page.evaluate( 'document.querySelector(".job-detail-box").__vue__.$store.state.userInfo' ) + const jobSource = await getCurrentJobSource() gtag('new_chat_startup', { chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList, - encryptJobId: currentJobData?.jobInfo?.encryptId + encryptJobId: currentJobData?.jobInfo?.encryptId, + jobSource: JobSource[jobSource] }) await saveChatStartupRecord( await dbInitPromise, @@ -142,7 +193,8 @@ const attachRequestsListener = async (target: Target) => { encryptUserId: currentUserInfo.encryptUserId }, { - chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList + chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList, + jobSource: JobSource[jobSource] } ) } else if ( diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts index 04d6ede..c055eb1 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts @@ -149,6 +149,16 @@ export default function initIpc() { bossConfig.strategyScopeOptionWhenMarkJobWorkExpNotMatch = payload.strategyScopeOptionWhenMarkJobWorkExpNotMatch } + if (hasOwn(payload, 'jobDetailRegExpMatchLogic')) { + bossConfig.jobDetailRegExpMatchLogic = payload.jobDetailRegExpMatchLogic + } + if (hasOwn(payload, 'isSkipEmptyConditionForCombineRecommendJobFilter')) { + bossConfig.isSkipEmptyConditionForCombineRecommendJobFilter = + payload.isSkipEmptyConditionForCombineRecommendJobFilter + } + if (hasOwn(payload, 'jobSourceList')) { + bossConfig.jobSourceList = payload.jobSourceList + } promiseArr.push(writeConfigFile('boss.json', bossConfig)) diff --git a/packages/ui/src/main/utils/gtag/GtagPlugin.ts b/packages/ui/src/main/utils/gtag/GtagPlugin.ts index e0401d0..de3c28e 100644 --- a/packages/ui/src/main/utils/gtag/GtagPlugin.ts +++ b/packages/ui/src/main/utils/gtag/GtagPlugin.ts @@ -2,26 +2,34 @@ import gtag from '.' export default class GtagPlugin { apply(hooks) { - hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom }) => { + hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom, jobSource }) => { gtag('new_chat_startup', { chatStartupFrom, - encryptJobId: jobData.jobInfo.encryptId - }) - }) - hooks.jobMarkedAsNotSuit.tap('GtagPlugin', (jobData, { markFrom, markOp, markReason }) => { - gtag('job_marked_as_not_suit', { - markFrom, - markOp, - markReason, - bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc, - encryptJobId: jobData.jobInfo.encryptId + encryptJobId: jobData.jobInfo.encryptId, + jobSource }) }) + hooks.jobMarkedAsNotSuit.tap( + 'GtagPlugin', + (jobData, { markFrom, markOp, markReason, jobSource }) => { + gtag('job_marked_as_not_suit', { + markFrom, + markOp, + markReason, + bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc, + encryptJobId: jobData.jobInfo.encryptId, + jobSource + }) + } + ) hooks.noPositionFoundForCurrentJob.tap('GtagPlugin', () => { gtag('no_position_found_for_current_job') }) hooks.noPositionFoundAfterTraverseAllJob.tap('GtagPlugin', () => { gtag('no_position_found_after_traverse_all_job') }) + hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => { + gtag('encounter_empty_rec_job_list', { pageQuery }) + }) } } diff --git a/packages/ui/src/renderer/src/features/JobSourceDragOrderer/index.vue b/packages/ui/src/renderer/src/features/JobSourceDragOrderer/index.vue new file mode 100644 index 0000000..ce764cc --- /dev/null +++ b/packages/ui/src/renderer/src/features/JobSourceDragOrderer/index.vue @@ -0,0 +1,357 @@ + + + + + diff --git a/packages/ui/src/renderer/src/page/CookieAssistant/index.vue b/packages/ui/src/renderer/src/page/CookieAssistant/index.vue index f0a01f8..2df9a10 100644 --- a/packages/ui/src/renderer/src/page/CookieAssistant/index.vue +++ b/packages/ui/src/renderer/src/page/CookieAssistant/index.vue @@ -109,8 +109,14 @@ import { ElForm, ElMessage } from 'element-plus' import { ref, onUnmounted, onMounted } from 'vue' import { checkCookieListFormat } from '../../../../common/utils/cookie' import { useRouter } from 'vue-router' -import { gtagRenderer } from '@renderer/utils/gtag' +import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag' +const gtagRenderer = (name, params?: object) => { + return baseGtagRenderer(name, { + scene: 'cookie-assistant', + ...params + }) +} const router = useRouter() const cookieInvalid = ref(false) diff --git a/packages/ui/src/renderer/src/page/FirstRunReadme/index.vue b/packages/ui/src/renderer/src/page/FirstRunReadme/index.vue index fa7599d..1c088cf 100644 --- a/packages/ui/src/renderer/src/page/FirstRunReadme/index.vue +++ b/packages/ui/src/renderer/src/page/FirstRunReadme/index.vue @@ -1,5 +1,5 @@