diff --git a/.github/workflows/release-ui.yml b/.github/workflows/release-ui.yml index 424236d..76523f0 100644 --- a/.github/workflows/release-ui.yml +++ b/.github/workflows/release-ui.yml @@ -60,9 +60,9 @@ jobs: strategy: matrix: platform: - - os: macos-13 # for x64 build + - os: macos-15-intel # for x64 build arch: x64 - - os: macos-14 # for arm64 build + - os: macos-15 # for arm64 build arch: arm64 env: PUPPETEER_SKIP_DOWNLOAD: 'true' diff --git a/packages/geek-auto-start-chat-with-boss/combineCalculator.mjs b/packages/geek-auto-start-chat-with-boss/combineCalculator.mjs index 726066a..a5bb8e2 100644 --- a/packages/geek-auto-start-chat-with-boss/combineCalculator.mjs +++ b/packages/geek-auto-start-chat-with-boss/combineCalculator.mjs @@ -124,3 +124,43 @@ export function checkAnyCombineBossRecommendFilterHasCondition(value) { return !!value[k]?.length }) } + +export function getStaticCombineFilterKey(condition) { + const kAsO = {} + for (const key of Object.keys(condition ?? []).sort()) { + if (condition[key] === null || condition[key] === undefined) { + continue + } + kAsO[key] = condition[key] + } + return JSON.stringify(kAsO) +} + +export function formatStaticCombineFilters(rawStaticCombineRecommendJobFilterConditions) { + rawStaticCombineRecommendJobFilterConditions = JSON.parse(JSON.stringify(rawStaticCombineRecommendJobFilterConditions)) + const map = new Map() + for (const condition of rawStaticCombineRecommendJobFilterConditions ?? []) { + const key = getStaticCombineFilterKey(condition) + map.set(key, condition) + } + const conditions = Array.from(map.values()) + const result = conditions.map((condition) => { + return { + salaryList: condition.salary ? [condition.salary] : [], + experienceList: condition.experience ? [condition.experience] : [], + degreeList: condition.degree ? [condition.degree] : [], + scaleList: condition.scale ? [condition.scale] : [], + industryList: condition.industry ? [condition.industry] : [] + } + }) + if (!result.length) { + result.push({ + salaryList: [], + experienceList: [], + degreeList: [], + scaleList: [], + industryList: [] + }) + } + return result +} 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 86602cd..6c9955d 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 @@ -1,4 +1,5 @@ { + "combineRecommendJobFilterType": 1, "anyCombineRecommendJobFilter": { "salaryList": [], "experienceList": [], @@ -6,6 +7,7 @@ "scaleList": [], "industryList": [] }, + "staticCombineRecommendJobFilterConditions": [], "isSkipEmptyConditionForCombineRecommendJobFilter": false, "expectJobRegExpStr": "", "jobNotMatchStrategy": 1, @@ -29,5 +31,8 @@ "type": "expect", "enabled": true } - ] + ], + "isSageTimeEnabled": true, + "sageTimeOpTimes": 100, + "sageTimePauseMinute": 15 } \ 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 73cc923..9224f18 100644 --- a/packages/geek-auto-start-chat-with-boss/index.mjs +++ b/packages/geek-auto-start-chat-with-boss/index.mjs @@ -15,12 +15,21 @@ import { readConfigFile, writeStorageFile, ensureConfigFileExist, readStorageFil import { calculateTotalCombinations, combineFiltersWithConstraintsGenerator, - checkAnyCombineBossRecommendFilterHasCondition + 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 } from '@geekgeekrun/sqlite-plugin/dist/enums' +import { + MarkAsNotSuitReason, + MarkAsNotSuitOp, + StrategyScopeOptionWhenMarkJobNotMatch, + SalaryCalculateWay, + JobDetailRegExpMatchLogic, + JobSource, + CombineRecommendJobFilterType +} from '@geekgeekrun/sqlite-plugin/dist/enums' import { activeDescList, RECOMMEND_JOB_ENTRY_SELECTOR, @@ -28,6 +37,7 @@ import { SEARCH_BOX_SELECTOR, } from './constant.mjs' import { parseSalary } from "@geekgeekrun/sqlite-plugin/dist/utils/parser" +import { waitForSageTimeOrJustContinue } from './sage-time.mjs' const jobFilterConditionsMapByCode = {} Object.values(jobFilterConditions).forEach(arr => { arr.forEach(option => { @@ -85,8 +95,10 @@ 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 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 @@ -108,7 +120,18 @@ const isSalaryFilterEnabled = expectSalaryLow || expectSalaryHigh const strategyScopeOptionWhenMarkSalaryNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkSalaryNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB // work exp -const expectWorkExpList = readConfigFile('boss.json').expectWorkExpList ?? [] +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 @@ -271,8 +294,8 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) { 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*="公司"]`)) ?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`)) ?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`)) if (opProxy) { @@ -282,10 +305,10 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) { break } case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: { - const opProxy = (await chooseReasonDialogProxy.$(`xpath///*[contains(@class,'zp-type-item')][contains(@title, "薪资")]`)) + 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*="公司"]`)) ?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`)) ?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`)) if (opProxy) { @@ -296,11 +319,11 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) { } case MarkAsNotSuitReason.JOB_NOT_SUIT: default: { - const jobNotSuitOptionProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="职位"]`)) + const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="职位"]`)) ?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`)) ?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`)) - if (jobNotSuitOptionProxy) { - await jobNotSuitOptionProxy.click() + if (opProxy) { + await opProxy.click() isOptionChosen = true } break @@ -347,24 +370,24 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) { let isJobNameSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true try { if (expectJobNameRegExpStr.trim()) { - const regExp = new RegExp(expectJobNameRegExpStr, 'i') - isJobNameSuit = regExp.test(jobInfo.jobName) + 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, 'i') - isJobTypeSuit = regExp.test(jobInfo.positionName) + 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, 'i') - isJobDescSuit = regExp.test(jobInfo.postDescription) + const regExp = new RegExp(expectJobDescRegExpStr, 'im') + isJobDescSuit = regExp.test(jobInfo.postDescription?.replace(/\n/g, '') ?? '') } } catch { } @@ -513,10 +536,11 @@ async function toRecommendPage (hooks) { }).then((res) => { return res.json() }) - await Promise.all([ - page.goto(recommendJobPageUrl, { timeout: 120 * 1000 }), - page.waitForNavigation(), - ]) + 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') @@ -651,11 +675,13 @@ async function toRecommendPage (hooks) { } } + const filterConditions = + combineRecommendJobFilterType === CombineRecommendJobFilterType.STATIC_COMBINE + ? formatStaticCombineFilters(staticCombineRecommendJobFilterConditions) + : combineFiltersWithConstraintsGenerator(anyCombineRecommendJobFilter) let expectJobList iterateFilterCondition: for ( - const filterCondition of combineFiltersWithConstraintsGenerator( - anyCombineRecommendJobFilter - ) + const filterCondition of filterConditions ) { findInCurrentFilterCondition: while(true) { await sleepWithRandomDelay(2500) @@ -680,11 +706,17 @@ async function toRecommendPage (hooks) { break } } - if ( - isSkipEmptyConditionForCombineRecommendJobFilter && - Object.keys(filterCondition).length && - Object.keys(filterCondition).every(k => !filterCondition[k]?.length) + ( + 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 @@ -707,6 +739,10 @@ async function toRecommendPage (hooks) { ); await storeStorage(page).catch(() => void 0) await sleepWithRandomDelay(2000) + await waitForSageTimeOrJustContinue({ + tag: 'afterJobSourceChosen', + hooks + }) } await sleepWithRandomDelay(1500) await setFilterCondition(filterCondition) @@ -807,7 +843,7 @@ async function toRecommendPage (hooks) { if (expectSalaryHigh && salaryData.high > expectSalaryHigh) { return false } - if (expectSalaryLow && salaryData.low < expectSalaryLow) { + if (expectSalaryLow && salaryData.high < expectSalaryLow) { return false } } else if (expectSalaryCalculateWay === SalaryCalculateWay.ANNUAL_PACKAGE) { @@ -815,7 +851,7 @@ async function toRecommendPage (hooks) { if (expectSalaryHigh && (salaryData.high * salaryDataMonth) / 10 > expectSalaryHigh) { return false } - if (expectSalaryLow && (salaryData.low * salaryDataMonth) / 10 < expectSalaryLow) { + if (expectSalaryLow && (salaryData.high * salaryDataMonth) / 10 < expectSalaryLow) { return false } } @@ -900,7 +936,10 @@ async function toRecommendPage (hooks) { } } requestNextPagePromiseWithResolver = null - + await waitForSageTimeOrJustContinue({ + tag: 'afterJobListPageFetched', + hooks + }) await sleep(5000) await updateJobListData() tempTargetJobIndexToCheckDetail = getTempTargetJobIndexToCheckDetail() @@ -944,6 +983,10 @@ async function toRecommendPage (hooks) { ); 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 @@ -971,6 +1014,10 @@ async function toRecommendPage (hooks) { } 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, @@ -985,7 +1032,8 @@ async function toRecommendPage (hooks) { jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) - } catch { + } catch(err) { + console.log(`mark boss inactive failed`, err) } } }, @@ -1008,6 +1056,10 @@ async function toRecommendPage (hooks) { } 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, @@ -1021,7 +1073,8 @@ async function toRecommendPage (hooks) { jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) - } catch { + } catch(err) { + console.log(`mark job city not suit failed`, err) } } }, @@ -1044,6 +1097,10 @@ async function toRecommendPage (hooks) { } 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, @@ -1057,7 +1114,8 @@ async function toRecommendPage (hooks) { jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) - } catch { + } catch(err) { + console.log(`mark job work exp not suit failed`, err) } } }, @@ -1080,6 +1138,10 @@ async function toRecommendPage (hooks) { } 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, @@ -1094,7 +1156,8 @@ async function toRecommendPage (hooks) { jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) - } catch { + } catch(err) { + console.log(`mark job detail not suit failed`, err) } } }, @@ -1119,6 +1182,10 @@ async function toRecommendPage (hooks) { } 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, @@ -1133,7 +1200,8 @@ async function toRecommendPage (hooks) { jobSource: JobSource[computedSourceList[currentSourceIndex]?.type] } ) - } catch { + } catch(err) { + console.log(`mark job salary not suit failed`, err) } } } @@ -1188,6 +1256,12 @@ async function toRecommendPage (hooks) { 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 @@ -1229,6 +1303,10 @@ async function toRecommendPage (hooks) { 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()') @@ -1238,28 +1316,21 @@ async function toRecommendPage (hooks) { //#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 + 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 } - 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 { + ); + const res = await addFriendResponse.json() + return res + } + const waitAndHandleChatSuccess = async () => { await hooks.newChatStartup?.promise( targetJobData, { @@ -1275,6 +1346,58 @@ async function toRecommendPage (hooks) { await closeDialogButtonProxy.click() await sleepWithRandomDelay(2000) } + const handleAddFriendResponse = async (res) => { + if (res.code === 0) { + await waitAndHandleChatSuccess() + } + 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') + } + } + const res = await waitAddFriendResponse() + await handleAddFriendResponse(res) // #endregion } catch (err) { if (err instanceof Error) { diff --git a/packages/geek-auto-start-chat-with-boss/sage-time.mjs b/packages/geek-auto-start-chat-with-boss/sage-time.mjs new file mode 100644 index 0000000..bc93171 --- /dev/null +++ b/packages/geek-auto-start-chat-with-boss/sage-time.mjs @@ -0,0 +1,54 @@ +import { readConfigFile } from './runtime-file-utils.mjs' +import { sleep } from "@geekgeekrun/utils/sleep.mjs" +let { + isSageTimeEnabled, + sageTimeOpTimes, + sageTimePauseMinute, +} = readConfigFile('boss.json') +isSageTimeEnabled = isSageTimeEnabled ?? true +sageTimeOpTimes = + isNaN(parseInt(sageTimeOpTimes)) || + parseInt(sageTimeOpTimes) < 1 + ? 100 + : parseInt(sageTimeOpTimes) +sageTimePauseMinute = + isNaN(parseFloat(sageTimePauseMinute)) || + parseFloat(sageTimePauseMinute) < 0 + ? 15 + : parseFloat(sageTimePauseMinute) +if (parseFloat(sageTimePauseMinute) === 0) { + isSageTimeEnabled = false +} + +let totalEnabledTimes = 0 +let recordedOpCount = 0 +export const waitForSageTimeOrJustContinue = async ({ + tag, + hooks, +} = {}) => { + if (!isSageTimeEnabled) { + return + } + const tagText = tag ? ` ${tag}` : '' + if (recordedOpCount > sageTimeOpTimes) { + totalEnabledTimes++ + console.log(`[SageTime${tagText}] 请求已达限制,开启;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次开启`) + await hooks?.sageTimeEnter?.promise({ + tag, + totalEnabledTimes, + recordedOpCount, + }) + await sleep(sageTimePauseMinute * 60 * 1000) + console.log(`[SageTime${tagText}] 请求限制已解除,关闭;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次关闭`) + await hooks?.sageTimeExit?.promise({ + tag, + totalEnabledTimes, + recordedOpCount, + }) + recordedOpCount = 0 + } + else { + console.log(`[SageTime${tagText}] 请求未达限制;当前记录次数 ${recordedOpCount};已开启过 ${totalEnabledTimes} 次`) + recordedOpCount++ + } +} \ No newline at end of file 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 7945286..8b75b90 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 @@ -57,7 +57,9 @@ const main = async () => { noPositionFoundForCurrentJob: new SyncHook(), noPositionFoundAfterTraverseAllJob: new SyncHook(), errorEncounter: new SyncHook(['errorInfo']), - encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']) + encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']), + sageTimeEnter: new AsyncSeriesHook(['args']), + sageTimeExit: new AsyncSeriesHook(['args']) } initPlugins(hooks) await hooks.daemonInitialized.callAsync() diff --git a/packages/sqlite-plugin/src/enums.ts b/packages/sqlite-plugin/src/enums.ts index ca74232..d70ab1e 100644 --- a/packages/sqlite-plugin/src/enums.ts +++ b/packages/sqlite-plugin/src/enums.ts @@ -34,3 +34,8 @@ export enum JobSource { recommend = 2, search = 3, } + +export enum CombineRecommendJobFilterType { + ANY_COMBINE = 1, + STATIC_COMBINE = 2, +} \ No newline at end of file diff --git a/packages/ui/electron.vite.config.ts b/packages/ui/electron.vite.config.ts index 4ae6177..5939052 100644 --- a/packages/ui/electron.vite.config.ts +++ b/packages/ui/electron.vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite' import vue from '@vitejs/plugin-vue' import UnoCSS from 'unocss/vite' import { presetUno, presetAttributify, presetIcons } from 'unocss' -import transformerDirective from "@unocss/transformer-directives"; +import transformerDirective from '@unocss/transformer-directives' import Replace from 'unplugin-replace/vite' process.env = { ...process.env, ...loadEnv(process.env.NODE_ENV!, process.cwd()) } @@ -13,11 +13,17 @@ export default defineConfig({ rollupOptions: { external: [] }, - minify: 'terser' + minify: process.env.NODE_ENV === 'development' ? undefined : 'terser' }, plugins: [ externalizeDepsPlugin({ - exclude: ['@geekgeekrun/geek-auto-start-chat-with-boss', '@geekgeekrun/dingtalk-plugin', '@geekgeekrun/utils', 'find-chrome-bin', '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'] + exclude: [ + '@geekgeekrun/geek-auto-start-chat-with-boss', + '@geekgeekrun/dingtalk-plugin', + '@geekgeekrun/utils', + 'find-chrome-bin', + '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension' + ] }), Replace({ delimiters: ['', ''], @@ -26,11 +32,11 @@ export default defineConfig({ values: [ { find: //g, - replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string, + replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string }, { find: //g, - replacement: process.env.VITE_APP_GTAG_API_SECRET as string, + replacement: process.env.VITE_APP_GTAG_API_SECRET as string } ] }) @@ -39,7 +45,7 @@ export default defineConfig({ preload: { plugins: [externalizeDepsPlugin()], build: { - minify: 'terser' + minify: process.env.NODE_ENV === 'development' ? undefined : 'terser' } }, renderer: { @@ -52,11 +58,11 @@ export default defineConfig({ vue(), UnoCSS({ presets: [presetUno(), presetAttributify(), presetIcons()], - transformers: [transformerDirective()], + transformers: [transformerDirective()] }) ], build: { - minify: 'terser' + minify: process.env.NODE_ENV === 'development' ? undefined : 'terser' } } }) diff --git a/packages/ui/package.json b/packages/ui/package.json index 0f71b88..53aaf9d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "geekgeekrun-ui", - "version": "0.9.2", + "version": "0.11.0", "description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!", "main": "./out/main/index.js", "author": "geekgeekrun", diff --git a/packages/ui/src/common/build-info.json b/packages/ui/src/common/build-info.json index 0a39c6a..a190e1e 100644 --- a/packages/ui/src/common/build-info.json +++ b/packages/ui/src/common/build-info.json @@ -1,7 +1,7 @@ { - "version": "0.9.2", - "buildVersion": 18, - "buildTime": 1755400602067, - "buildHash": "a25b047f99f7b1ac0c8ee55f1e121991e1dd6402", + "version": "0.11.0", + "buildVersion": 25, + "buildTime": 1765792961398, + "buildHash": "2b4b62097730bf0388f2179c02fdead281ec26fc", "name": "geekgeekrun-ui" } \ No newline at end of file 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 5108946..1387f1f 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 @@ -1,5 +1,5 @@ import DingtalkPlugin from '@geekgeekrun/dingtalk-plugin/index.mjs' -import { app } from 'electron' +import { app, dialog } from 'electron' import { SyncHook, AsyncSeriesHook } from 'tapable' import { readConfigFile, @@ -94,7 +94,9 @@ const runAutoChat = async () => { noPositionFoundForCurrentJob: new SyncHook(), noPositionFoundAfterTraverseAllJob: new SyncHook(), errorEncounter: new SyncHook(['errorInfo']), - encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']) + encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']), + sageTimeEnter: new AsyncSeriesHook(['args']), + sageTimeExit: new AsyncSeriesHook(['args']) } initPlugins(hooks) @@ -121,6 +123,11 @@ const runAutoChat = async () => { } catch (err) { if (err instanceof Error) { if (err.message.includes('LOGIN_STATUS_INVALID')) { + await dialog.showMessageBox({ + type: `error`, + message: `登录状态无效`, + detail: `请重新登录Boss直聘` + }) process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID) break } 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 c055eb1..5b30682 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 @@ -159,6 +159,22 @@ export default function initIpc() { if (hasOwn(payload, 'jobSourceList')) { bossConfig.jobSourceList = payload.jobSourceList } + if (hasOwn(payload, 'combineRecommendJobFilterType')) { + bossConfig.combineRecommendJobFilterType = payload.combineRecommendJobFilterType + } + if (hasOwn(payload, 'staticCombineRecommendJobFilterConditions')) { + bossConfig.staticCombineRecommendJobFilterConditions = + payload.staticCombineRecommendJobFilterConditions + } + if (hasOwn(payload, 'isSageTimeEnabled')) { + bossConfig.isSageTimeEnabled = payload.isSageTimeEnabled + } + if (hasOwn(payload, 'sageTimeOpTimes')) { + bossConfig.sageTimeOpTimes = payload.sageTimeOpTimes + } + if (hasOwn(payload, 'sageTimePauseMinute')) { + bossConfig.sageTimePauseMinute = payload.sageTimePauseMinute + } 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 de3c28e..2a4170a 100644 --- a/packages/ui/src/main/utils/gtag/GtagPlugin.ts +++ b/packages/ui/src/main/utils/gtag/GtagPlugin.ts @@ -31,5 +31,11 @@ export default class GtagPlugin { hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => { gtag('encounter_empty_rec_job_list', { pageQuery }) }) + hooks.sageTimeEnter.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => { + gtag('sage_time_enter', { tag, totalEnabledTimes, recordedOpCount }) + }) + hooks.sageTimeExit.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => { + gtag('sage_time_exit', { tag, totalEnabledTimes, recordedOpCount }) + }) } } diff --git a/packages/ui/src/renderer/src/features/StaticCombineBossRecommendFilter/index.vue b/packages/ui/src/renderer/src/features/StaticCombineBossRecommendFilter/index.vue new file mode 100644 index 0000000..56ef046 --- /dev/null +++ b/packages/ui/src/renderer/src/features/StaticCombineBossRecommendFilter/index.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss/expectJobFilterTemplateList.ts b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss/expectJobFilterTemplateList.ts new file mode 100644 index 0000000..51c5925 --- /dev/null +++ b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss/expectJobFilterTemplateList.ts @@ -0,0 +1,233 @@ +import { JobDetailRegExpMatchLogic } from '@geekgeekrun/sqlite-plugin/src/enums' + +const expectJobFilterTemplateList = [ + { + type: '不限职位', + name: '不限职位(随便投)', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '互联网/AI', + name: 'Java', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '\\bJava\\b', + expectJobDescRegExpStr: + '\\bJava\\b|JVM|消息队列|MQ|SQL|Oracle|MongoDB|Redis|Nginx|Dubbo|Docker|K8s|Kubernetes', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '互联网/AI', + name: 'Golang', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '\\bGolang\\b', + expectJobDescRegExpStr: + '\\bGo\\b|\\bGolang\\b|消息队列|MQ|SQL|Oracle|MongoDB|Redis|Nginx|Dubbo|Docker|K8s|Kubernetes', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '互联网/AI', + name: '前端开发工程师', + config: { + expectJobNameRegExpStr: '前端|H5|\\bFE\\b', + expectJobTypeRegExpStr: '前端开发|javascript', + expectJobDescRegExpStr: '前端|vue|react|node|\\bjs\\b|javascript|H5', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '互联网/AI', + name: '前端开发工程师(不考虑外包、兼职)', + config: { + expectJobNameRegExpStr: '^(?=.*(前端|H5|\\bFE\\b))(?!.*(?:外包|驻场|外派|兼职|短期))', + expectJobTypeRegExpStr: '前端开发|javascript', + expectJobDescRegExpStr: + '^(?=.*(前端|vue|react|node|\\bjs\\b|javascript|H5))(?!.*(?:外包|驻场|外派|兼职|短期))', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '互联网/AI', + name: '测试工程师、测试开发', + config: { + expectJobNameRegExpStr: '测试|测开|QA|质量', + expectJobTypeRegExpStr: '测试工程师|测试开发', + expectJobDescRegExpStr: + '测试|测开|QA|线上问题|自动化|复盘|效率|Selenium|Puppeteer|Playwright|Cypress|JMeter|LoadRunner|QTP|TestNG|JUnit|Pytest|Fiddler|Charles|Jenkins|Appium|黑盒|白盒|用例|缺陷|Linux|Ubuntu|Debian|CentOS|Shell|c\\+\\+|Python|PHP|\\bJava\\b|Node|\\bGo\\b|\\bGolang\\b', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '互联网/AI', + name: '运维工程师、运维开发工程师', + config: { + expectJobNameRegExpStr: '运维(开发)?|SRE', + expectJobTypeRegExpStr: '运维(开发)?工程师', + expectJobDescRegExpStr: + '运维|SRE|服务器|云计算|Docker|K8s|Kubernetes|Linux|Ubuntu|Debian|CentOS|Shell|Python|\\bGo\\b|\\bGolang\\b|监控|Prometheus|Grafana|ELK|负载均衡|部署|Nginx|Apache|DevOps|harbor', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '互联网/AI', + name: '数据开发', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '数据开发', + expectJobDescRegExpStr: 'c\\+\\+|Python|\\bGo\\b|\\bGolang\\b|\\bJava\\b|Node|数据仓库|ETL|大数据|Hadoop|Spark|Flink|Hive|Presto|数据湖|数仓|SQL|Oracle|MongoDB|Redis|Kafka', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '互联网/AI', + name: '实施工程师', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '实施', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '产品', + name: '产品经理', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '产品经理', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '产品', + name: '用户研究', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '用户研究', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '产品', + name: '游戏策划', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '游戏策划', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '客服/运营', + name: '产品运营', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '产品运营', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '客服/运营', + name: '用户运营', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '用户运营', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '客服/运营', + name: '数据标注/AI训练师', + config: { + expectJobNameRegExpStr: '', + expectJobTypeRegExpStr: '数据标注|AI训练师', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '财务/审计/税务', + name: '会计、出纳', + config: { + expectJobNameRegExpStr: '会计|Accountant|出纳|财务', + expectJobTypeRegExpStr: '会计|出纳', + expectJobDescRegExpStr: + '会计|财务|出纳|审计|账务|税务|总账|做账|应付|应收|成本|资产|资金|记账|发票|结算|核算|汇算|利润|对账|报税|回款|SAP|用友|金蝶', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '人力/行政/法务', + name: '人力资源专员/助理、人力资源经理/主管', + config: { + expectJobNameRegExpStr: 'HR|人力|人资|人事', + expectJobTypeRegExpStr: '人力', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '人力/行政/法务', + name: 'HRBP', + config: { + expectJobNameRegExpStr: 'BP|HRG|HR|人力|人资|人事', + expectJobTypeRegExpStr: 'HRBP|人力', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '人力/行政/法务', + name: '员工关系', + config: { + expectJobNameRegExpStr: '员工关系|劳动关系|SSC|社保|HR|人力|人资|人事', + expectJobTypeRegExpStr: '员工关系|人力', + expectJobDescRegExpStr: '员工关系|劳动关系|SSC|社保|考勤|入职|离职|入转调离', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '人力/行政/法务', + name: '招聘', + config: { + expectJobNameRegExpStr: '招聘|高招|Recruiter|HR|人力|人资|人事', + expectJobTypeRegExpStr: '招聘|猎头|人力', + expectJobDescRegExpStr: + '招聘|高招|Recruiter|简历|面试|人才引进|Mapping|人才画像|offer|猎头|内推|外推|猎聘|Boss|拉勾|前程无忧|智联|58同城|领英|LinkedIn|ATS|人才库|Moka|北森|iTenant|倍罗|大易', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY + } + }, + { + type: '人力/行政/法务', + name: '薪酬绩效', + config: { + expectJobNameRegExpStr: '薪酬|绩效|福利|COE|payroll', + expectJobTypeRegExpStr: '薪酬绩效', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, + { + type: '人力/行政/法务', + name: '企业文化', + config: { + expectJobNameRegExpStr: '企业文化|组织文化|组织|OC|廉洁|反腐', + expectJobTypeRegExpStr: '企业文化', + expectJobDescRegExpStr: '', + jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME + } + }, +] + +export default expectJobFilterTemplateList diff --git a/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss/index.vue b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss/index.vue index 7a80ce2..60e5f8e 100644 --- a/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss/index.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss/index.vue @@ -8,7 +8,7 @@
- +
BOSS直聘 Cookie
+
+
+ 摸鱼模式 + + + 这个配置会对本程序运行过程造成什么影响? + +
+
+ + 启用摸鱼模式 + +
+
+
+ 当如下行为的次数总计达 + + + + 次时,暂停运行 + + + + 分钟: +
    +
  • 职位来源变更
  • +
  • 职位列表滚动后发生加载
  • +
  • 职位详情加载
  • +
  • 职位被标记不合适
  • +
  • 职位被开聊
  • +
+
之后继续运行并重新计次,循环整个过程
+
+
+
- 职位来源及其查找顺序 + 你想投递Boss直聘上哪些列表里的职位?
- 拖放条目前方的手柄以调整职位来源查找顺序;点击条目前方的开关以启用/禁用对应的职位来源 + 拖放条目前方的手柄以调整职位列表查找顺序;点击条目前方的开关以启用/禁用对应的职位列表
- - - -
- 职位筛选条件 - - - 这个配置是如何工作的? +
+
+ 你希望Boss直聘为你筛选出什么样的职位? + - + + 这个配置是如何工作的? + +
+
+
+
筛选条件遍历方式
+ + {{ op.name }} + +
+
+ +
+
+ +
+ + 跳过初始空条件,直接使用设置的条件查找职位 + + + 跳过初始空条件,直接使用设置的条件查找职位 + +
+
+
+
+ 当前组合条件数:{{ + currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString() + }} + 不建议选择太多组合条件 - + 否则将在当前职位中尝试太多筛选条件,不能及时进入下一个职位,且会增加命中风控的概率 +
- - - - 跳过初始空条件,直接使用设置的条件查找职位 - - - 跳过初始空条件,直接使用设置的条件查找职位 - - -
- 当前组合条件数:{{ - currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString() - }} - 不建议选择太多组合条件 - - 否则将在当前职位中尝试太多筛选条件,不能及时进入下一个职位,且会增加命中风控的概率 -