Compare commits

...

31 Commits

Author SHA1 Message Date
geekgeekrun-maintainer
9324f20ad3 Update job search timeline and experiences 2026-01-09 16:49:07 +08:00
geekgeekrun-maintainer
632a11b105 更新 README.md 2026-01-09 13:57:08 +08:00
geekgeekrun-maintainer
373e45dee8 update readme 2025-12-24 16:35:17 +08:00
geekgeekrun
cf7a0d3902 Merge branch 'feature/ui' 2025-12-17 14:32:56 +08:00
geekgeekrun
c95349c037 Merge remote-tracking branch 'origin/master' into feature/ui 2025-12-16 23:07:02 +08:00
geekgeekrun-maintainer
8197782102 Replace old image with new one in README
Updated image in README.md to a new source.
2025-12-15 22:59:44 +08:00
geekgeekrun
f17d626ea0 ui-v0.11.0 2025-12-15 18:02:41 +08:00
geekgeekrun
2b4b620977 fix match rule for multi line in testIfJobTitleOrDescriptionSuit 2025-12-15 18:01:52 +08:00
geekgeekrun
03e3c3661f adjust default config of sage time 2025-12-14 20:59:50 +08:00
geekgeekrun
5d787fdd50 add sage time intro 2025-12-14 13:22:56 +08:00
geekgeekrun
b68ff63b6b add sage time gtag in renderer process 2025-12-14 12:08:11 +08:00
geekgeekrun
2aee76307f add sage time gtag in main process 2025-12-14 11:15:22 +08:00
geekgeekrun
e2040caa38 add sage time ui, make it configurable 2025-12-14 10:46:19 +08:00
geekgeekrun
fba45e420f add sage time for pausing when request count to a time 2025-12-13 15:15:09 +08:00
geekgeekrun
fe629eaa22 ui-v0.10.2 2025-12-12 22:10:32 +08:00
geekgeekrun
056ec7a0a6 handle dialog about /猎头/ during startup chat 2025-12-12 22:03:56 +08:00
geekgeekrun
f27ca4145a handle dialog about /剩\d+次沟通机会/ during startup chat 2025-12-12 22:02:57 +08:00
geekgeekrun
cd09210017 add the tip dialog of LOGIN_STATUS_INVALID 2025-12-12 20:14:04 +08:00
geekgeekrun
aec72c28f0 ui-v0.10.1 2025-12-10 14:44:21 +08:00
geekgeekrun
69cf5fd34f upgrade macOS version in build ci workflow file 2025-12-10 14:44:01 +08:00
geekgeekrun
5f47e47448 upgrade macOS version in build ci workflow file 2025-12-10 13:26:29 +08:00
geekgeekrun
4e9b44d04c fix for misspell of scale property. 2025-12-10 13:16:15 +08:00
geekgeekrun
d206d45242 fix when return value of formatStaticCombineFilters is a empty array, program cannot run correctly 2025-12-10 13:15:55 +08:00
geekgeekrun
0aef2af350 ui-v0.10.0 2025-11-01 23:45:16 +08:00
geekgeekrun
c3dc75b503 ui-v0.9.5 2025-11-01 23:15:13 +08:00
geekgeekrun
7a1ae48aa7 Merge branch 'feature/static-combine-job-filter' into feature/ui 2025-11-01 23:14:47 +08:00
geekgeekrun
e5225006cc form validate stage change 2025-11-01 23:13:24 +08:00
geekgeekrun
fdf2c61620 support multi line regexp 2025-11-01 00:39:44 +08:00
geekgeekrun
1f140121fb handle duplicate static combine conditions 2025-10-26 19:12:13 +08:00
geekgeekrun
57cf631b86 add static combine job filter 2025-10-26 18:00:35 +08:00
geekgeekrun
36aef1c16d add more position presets 2025-10-24 01:42:30 +08:00
17 changed files with 1101 additions and 181 deletions

View File

@@ -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'

View File

@@ -1,5 +1,9 @@
# 牛人快跑 - GeekGeekRun
**->由于BOSS直聘于2026.1.8上线了反调试/反爬虫/反自动化逻辑本程序可能暂时无法使用目前正在尝试处理中有相关处理经验的朋友可以提交PR<-**
**->维护者正在疯狂找工作<-**
一款可以帮助你在Boss直聘上**自动批量开聊Boss**的脚本基于Puppeteer。
与每一位牛人站在一起
@@ -14,7 +18,7 @@
### Boss炸弹
扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的Boss。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位可以通过拖拽的方式自定义搜索顺序哦~
<img width="1280" height="2474" alt="Snipaste_2025-07-15_13-05-35" src="https://github.com/user-attachments/assets/30308ec2-1e3b-4a8f-b3fe-7d205394578d" />
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
#### 匹配步骤
@@ -136,10 +140,10 @@ Boss不明原因已读不回简历就是投不出去
### 个人而言
以我为例,我真的求职求累了
2023年初~2025年初,这段时间
- 求职经历:离职求职5个月在职求职3个月;曾面试66个岗位各种面试一面、二面、三面、HR面加起来134Offer 寥寥无几
- 被动失业经历1 次裁员1 次卡试用期
- 目前状态:工作(但愿明天也有工作
2023年初~2026年年初,这段时间
- 求职经历:离职求职7个月;曾面试73个岗位各种面试一面、二面、三面、HR面加起来156Offer 寥寥无几
- 被动失业经历1 次裁员1 次卡试用期1 次部门整体裁撤
- 目前状态:工作(**正在疯狂找工作**
如此多的经历已经让我成为了一位资深Boss用户
@@ -157,7 +161,7 @@ Boss不明原因已读不回简历就是投不出去
一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场
当然,如果运气好:
- 好不容易过了 HR 面,到了 Offer 阶段HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个不满意的 Offer
- 好不容易过了 HR 面,到了 Offer 阶段HR / 用人部门 几乎都在极限压缩用人成本,经常能被 ** 到,但你又没有其它更好 Offer ,不敢贸然放弃这个不满意的 Offer
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了不满意的 Offer 入了职然而经过一段时间的接触最终结果完全不符合期望诸如“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活而且要帮前人擦屁股”、“领导希望用最小成本把你招来这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
@@ -358,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 {
}
@@ -524,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')
@@ -662,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)
@@ -691,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
@@ -718,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)
@@ -911,7 +936,10 @@ async function toRecommendPage (hooks) {
}
}
requestNextPagePromiseWithResolver = null
await waitForSageTimeOrJustContinue({
tag: 'afterJobListPageFetched',
hooks
})
await sleep(5000)
await updateJobListData()
tempTargetJobIndexToCheckDetail = getTempTargetJobIndexToCheckDetail()
@@ -955,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
@@ -982,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,
@@ -1020,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,
@@ -1057,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,
@@ -1094,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,
@@ -1134,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,
@@ -1251,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()')
@@ -1260,19 +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) {
);
const res = await addFriendResponse.json()
return res
}
const waitAndHandleChatSuccess = async () => {
await hooks.newChatStartup?.promise(
targetJobData,
{
@@ -1288,28 +1346,58 @@ async function toRecommendPage (hooks) {
await closeDialogButtonProxy.click()
await sleepWithRandomDelay(2000)
}
// TODO:
// else if (res.zpData.bizCode === 1 && res.zpData.bizData?.chatRemindDialog?.blockLevel === 0 && /还剩\d+次沟通机会/.test(res.zpData.bizData?.chatRemindDialog?.content)) {
// debugger
// }
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 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) {

View File

@@ -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++
}
}

View File

@@ -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()

View File

@@ -34,3 +34,8 @@ export enum JobSource {
recommend = 2,
search = 3,
}
export enum CombineRecommendJobFilterType {
ANY_COMBINE = 1,
STATIC_COMBINE = 2,
}

View File

@@ -1,6 +1,6 @@
{
"name": "geekgeekrun-ui",
"version": "0.9.4",
"version": "0.11.0",
"description": "Boss 炸弹 - 自动开聊Boss助力每位打工人求职",
"main": "./out/main/index.js",
"author": "geekgeekrun",

View File

@@ -1,7 +1,7 @@
{
"version": "0.9.4",
"buildVersion": 20,
"buildTime": 1760377927509,
"buildHash": "51ca5dd71d62de2965fb53cff9d03c0d3552eabb",
"version": "0.11.0",
"buildVersion": 25,
"buildTime": 1765792961398,
"buildHash": "2b4b62097730bf0388f2179c02fdead281ec26fc",
"name": "geekgeekrun-ui"
}

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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 })
})
}
}

View File

@@ -0,0 +1,272 @@
<template>
<div>
<el-button p-0 h-auto flex size="small" type="text" :icon="Plus" @click="addCondition"
>添加条件</el-button
>
<div class="job-combo-filter" mt-8px>
<el-table
size="small"
:data="props.modelValue"
border
:style="{ maxWidth: '100%' }"
:row-style="
({ row }) => {
return {
backgroundColor:
duplicatedMap.get(getStaticCombineFilterKey(row))?.length > 1
? '#fcd4b7'
: 'transparent'
}
}
"
>
<template #empty>
<div lh-1.5em>
列表中没有条件,将仅使用默认的“初始空条件”为您筛选职位<br />
你可以点击表格左上角“<el-button
p-0
h-auto
size="small"
type="text"
:icon="Plus"
@click="addCondition"
>添加条件</el-button
>”按钮,添加更多筛选条件。
</div>
</template>
<el-table-column :resizable="false" label="" :width="80">
<template #default="{ $index: index }">
<div
:style="{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '2px',
height: 'fit-content'
}"
>
<el-button
:disabled="index <= 0"
style="margin: 0"
circle
size="small"
:icon="ArrowUp"
@click="moveConditionUp(index)"
/>
<el-button
:disabled="index >= modelValue?.length - 1"
style="margin: 0"
circle
size="small"
:icon="ArrowDown"
@click="moveConditionDown(index)"
/>
<el-button
style="margin: 0"
circle
size="small"
:icon="Delete"
@click="removeCondition(index)"
/>
</div>
</template>
</el-table-column>
<el-table-column :resizable="false" label="薪资待遇" prop="salary">
<template #default="{ row }">
<el-select
v-model="row.salary"
:disabled="row.___itemType === 'empty-condition-placeholder'"
clearable
size="small"
>
<el-option
v-for="it in conditions.salaryList.filter((it) => it.code !== 0)"
:key="it.code"
:value="it.code"
:label="it.name"
/>
</el-select>
</template>
</el-table-column>
<el-table-column :resizable="false" label="工作经验" prop="experience">
<template #default="{ row }">
<el-select
v-model="row.experience"
:disabled="row.___itemType === 'empty-condition-placeholder'"
clearable
size="small"
>
<el-option
v-for="it in conditions.experienceList.filter((it) => it.code !== 0)"
:key="it.code"
:value="it.code"
:label="it.name"
/>
</el-select>
</template>
</el-table-column>
<el-table-column :resizable="false" label="学历要求" prop="degree">
<template #default="{ row }">
<el-select
v-model="row.degree"
:disabled="row.___itemType === 'empty-condition-placeholder'"
clearable
size="small"
>
<el-option
v-for="it in conditions.degreeList.filter((it) => it.code !== 0)"
:key="it.code"
:value="it.code"
:label="it.name"
/>
</el-select>
</template>
</el-table-column>
<el-table-column :resizable="false" label="公司行业" :width="200" prop="industry">
<template #default="{ row }">
<el-select
v-model="row.industry"
:disabled="row.___itemType === 'empty-condition-placeholder'"
clearable
size="small"
>
<el-option-group
v-for="group in industryFilterExemption"
:key="group.code"
:label="group.name"
>
<el-option
v-for="item in group.subLevelModelList"
:key="item.code"
:label="item.name"
:value="item.code"
/>
</el-option-group>
</el-select>
</template>
</el-table-column>
<el-table-column :resizable="false" label="公司规模" prop="scale">
<template #default="{ row }">
<el-select
v-model="row.scale"
:disabled="row.___itemType === 'empty-condition-placeholder'"
clearable
size="small"
>
<el-option
v-for="it in conditions.scaleList.filter((it) => it.code !== 0)"
:key="it.code"
:value="it.code"
:label="it.name"
/>
</el-select>
</template>
</el-table-column>
</el-table>
</div>
<div
v-if="
Array.from(duplicatedMap.values()).some((it) => {
return it.length > 1
})
"
color-orange
font-size-12px
>
列表中被橙色高亮的条件存在重复项,相关重复项将被合并,运行时遍历顺序以第一次出现为准
</div>
</div>
</template>
<script lang="ts" setup>
import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-conditions-20241002.json'
import industryFilterExemption from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-industry-filter-exemption-20241002.json'
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
import { computed, PropType } from 'vue'
import { getStaticCombineFilterKey } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
const props = defineProps({
modelValue: {
type: Array as PropType<
Array<{
salary: number | null
experience: number | null
degree: number | null
industry: number | null
scale: number | null
}>
>,
default: () => []
},
isSkipEmptyConditionForCombineRecommendJobFilter: {
type: Boolean
}
})
// fix for misspell of scale property.
for (const condition of props.modelValue) {
if ((condition as any).scaleList && !condition.scale) {
condition.scale = (condition as any).scaleList
delete (condition as any).scaleList
}
}
function getNewConditionItem() {
return {
salary: null,
experience: null,
degree: null,
industry: null,
scale: null
}
}
function addCondition() {
props.modelValue?.push(getNewConditionItem())
// gtagRenderer('resume_work_exp_added')
}
function moveConditionUp(index) {
;[props.modelValue[index], props.modelValue[index - 1]] = [
props.modelValue[index - 1],
props.modelValue[index]
]
// gtagRenderer('resume_work_exp_moved_up')
}
function moveConditionDown(index) {
;[props.modelValue[index], props.modelValue[index + 1]] = [
props.modelValue[index + 1],
props.modelValue[index]
]
// gtagRenderer('resume_work_exp_moved_down')
}
function removeCondition(index) {
props.modelValue?.splice(index, 1)
// gtagRenderer('resume_work_exp_removed')
}
const duplicatedMap = computed(() => {
const map = new Map()
for (const condition of props.modelValue ?? []) {
const key = getStaticCombineFilterKey(condition)
if (!map.has(key)) {
map.set(key, [])
}
const arr = map.get(key)
arr.push(condition)
}
return map
})
</script>
<style lang="scss" scoped>
.job-combo-filter {
display: flex;
width: 100%;
gap: 10px;
.filter-item {
flex: 1;
}
}
</style>

View File

@@ -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

View File

@@ -8,7 +8,7 @@
<div class="form-wrap geek-auto-start-run-with-boss">
<el-form ref="formRef" :model="formContent" label-position="top" :rules="formRules">
<el-card class="config-section">
<el-form-item mb0>
<el-form-item>
<div>
<div font-size-16px>BOSS直聘 Cookie</div>
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
@@ -16,6 +16,100 @@
>
</div>
</el-form-item>
<div>
<div font-size-16px>
摸鱼模式
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_sage_t')"
>
<template #content>
<div>
本程序运行较长时间后Boss直聘会对账号进行风控导致本程序不能继续执行<br />
为此加入摸鱼模式通过此配置主动减慢本程序的运行速度降低潜在的被风控监测到的概率<br />
你可以自定义开启摸鱼模式的频率 - 默认为操作100次后暂停运行15分钟
</div>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>这个配置会对本程序运行过程造成什么影响</el-button
>
</el-tooltip>
</div>
<div>
<el-checkbox
v-model="formContent.isSageTimeEnabled"
@change="
(v) => {
gtagRenderer('sage_t_enable_changed', { v })
}
"
>
启用摸鱼模式
</el-checkbox>
</div>
<div pl-1.5em font-size-14px>
<div
:style="{
color: formContent.isSageTimeEnabled ? '' : '#aaa'
}"
>
当如下行为的次数总计达
<el-form-item mb0 inline-block prop="sageTimeOpTimes">
<el-input-number
v-model="formContent.sageTimeOpTimes"
:step="1"
step-strictly
:precision="0"
:min="1"
:disabled="!formContent.isSageTimeEnabled"
controls-position="right"
@change="
(v) => {
gtagRenderer('sage_t_op_times_changed', { v })
}
"
/>
</el-form-item>
次时暂停运行
<el-form-item mb0 inline-block prop="sageTimePauseMinute">
<el-input-number
v-model="formContent.sageTimePauseMinute"
:step="0.5"
step-strictly
:precision="1"
:min="0"
:disabled="!formContent.isSageTimeEnabled"
controls-position="right"
@change="
(v) => {
gtagRenderer('sage_t_pause_minute_changed', { v })
}
"
/>
</el-form-item>
分钟
<ul
pl-1em
mb0
mt14px
:style="{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
lineHeight: '1.5em'
}"
>
<li>职位来源变更</li>
<li>职位列表滚动后发生加载</li>
<li>职位详情加载</li>
<li>职位被标记不合适</li>
<li>职位被开聊</li>
</ul>
<div mt14px>之后继续运行并重新计次循环整个过程</div>
</div>
</div>
</div>
</el-card>
<el-card class="config-section">
<el-form-item class="job-source-form-item" prop="__jobSourceList">
@@ -49,60 +143,99 @@
/>
</div>
</el-form-item>
<el-form-item prop="filter" mt18px>
<div font-size-16px>
你希望Boss直聘为你展示什么样的职位
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_wrongly_mark_not_suit')"
>
<template #content>
<ul m0 line-height-1.5em w-540px pl2em>
<li>
这个配置决定了下面列表中会展示哪些职位查找职位时会在Boss直聘页面上自动组合下方设置的筛选条件对应UI<br />
<img h-270px src="../resources/intro-of-job-filter-auto-change.png" />
</li>
<li color-orange>
建议不要筛选过多条件否则最终组合数会成为一个很大的数字这将在当前职位中尝试太多筛选条件不能及时进入下一个职位且会增加命中风控的概率
</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>这个配置是如何工作的</el-button
<el-form-item prop="filter" mt18px mb0px w-full>
<div flex-1>
<div font-size-16px>
你希望Boss直聘为你筛选出什么样的职位
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_wrongly_mark_not_suit')"
>
</el-tooltip>
<template #content>
<ul m0 line-height-1.5em w-540px pl2em>
<li>
这个配置决定了下面列表中会展示哪些职位查找职位时会在Boss直聘页面上自动组合下方设置的筛选条件对应UI<br />
<img h-270px src="../resources/intro-of-job-filter-auto-change.png" />
</li>
<li color-orange>
建议不要筛选过多条件否则最终组合数会成为一个很大的数字这将在当前职位中尝试太多筛选条件不能及时进入下一个职位且会增加命中风控的概率
</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>这个配置是如何工作的</el-button
>
</el-tooltip>
</div>
<div>
<div>
<div font-size-12px>筛选条件遍历方式</div>
<el-select
v-model="formContent.combineRecommendJobFilterType"
w-320px
@change="
(v) => {
gtagRenderer('crjf_type_changed', { v })
}
"
>
<el-option
v-for="op in combineRecommendJobFilterTypeOptions"
:key="op.value"
:value="op.value"
:label="op.name"
>{{ op.name }}</el-option
>
</el-select>
</div>
<div
v-if="
formContent.combineRecommendJobFilterType ===
CombineRecommendJobFilterType.STATIC_COMBINE
"
mt8px
>
<StaticCombineBossRecommendFilter
v-model="formContent.staticCombineRecommendJobFilterConditions"
/>
</div>
<div v-else mt8px>
<AnyCombineBossRecommendFilter
v-model="formContent.anyCombineRecommendJobFilter"
/>
<div mb0>
<el-checkbox
v-if="anyCombineBossRecommendFilterHasCondition"
v-model="formContent.isSkipEmptyConditionForCombineRecommendJobFilter"
@change="
(v) => {
gtagRenderer('is_skip_empty_condition_4crjf_changed', { v })
}
"
>
<span font-size-12px>跳过初始空条件,直接使用设置的条件查找职位</span>
</el-checkbox>
<el-checkbox v-else :model-value="false" disabled>
<span font-size-12px>跳过初始空条件,直接使用设置的条件查找职位</span>
</el-checkbox>
</div>
</div>
</div>
<div font-size-12px>
当前组合条件数:{{
currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString()
}}
<span
v-if="currentAnyCombineRecommendJobFilterCombinationCount >= 5"
class="color-orange"
>不建议选择太多组合条件 -
否则将在当前职位中尝试太多筛选条件,不能及时进入下一个职位,且会增加命中风控的概率</span
>
</div>
</div>
<AnyCombineBossRecommendFilter v-model="formContent.anyCombineRecommendJobFilter" />
</el-form-item>
<el-form-item prop="filter" mb0>
<el-checkbox
v-if="anyCombineBossRecommendFilterHasCondition"
v-model="formContent.isSkipEmptyConditionForCombineRecommendJobFilter"
@change="
(v) => {
gtagRenderer('is_skip_empty_condition_4crjf_changed', { v })
}
"
>
<span font-size-12px>跳过初始空条件直接使用设置的条件查找职位</span>
</el-checkbox>
<el-checkbox v-else :model-value="false" disabled>
<span font-size-12px>跳过初始空条件直接使用设置的条件查找职位</span>
</el-checkbox>
</el-form-item>
<div font-size-12px mt10px>
当前组合条件数{{
currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString()
}}
<span
v-if="currentAnyCombineRecommendJobFilterCombinationCount >= 5"
class="color-orange"
>不建议选择太多组合条件 -
否则将在当前职位中尝试太多筛选条件不能及时进入下一个职位且会增加命中风控的概率</span
>
</div>
</el-card>
<!-- <el-form-item
label="钉钉机器人 AccessToken用于记录开聊请勿使用公司内部群"
@@ -613,7 +746,12 @@
<el-icon class="el-icon--right"><arrow-down /></el-icon
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-menu
:style="{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr'
}"
>
<el-dropdown-item
v-for="item in expectJobFilterTemplateList"
:key="item.name"
@@ -921,15 +1059,18 @@ import { ElForm, ElMessage } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import AnyCombineBossRecommendFilter from '@renderer/features/AnyCombineBossRecommendFilter/index.vue'
import StaticCombineBossRecommendFilter from '@renderer/features/StaticCombineBossRecommendFilter/index.vue'
import { activeDescList } from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs'
import {
calculateTotalCombinations,
checkAnyCombineBossRecommendFilterHasCondition
checkAnyCombineBossRecommendFilterHasCondition,
formatStaticCombineFilters
} from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import defaultTargetCompanyListConf from '@geekgeekrun/geek-auto-start-chat-with-boss/default-config-file/target-company-list.json'
import { ArrowDown } from '@element-plus/icons-vue'
import {
CombineRecommendJobFilterType,
MarkAsNotSuitOp,
StrategyScopeOptionWhenMarkJobNotMatch,
SalaryCalculateWay,
@@ -940,6 +1081,7 @@ import mittBus from '../../../utils/mitt'
import CityChooser from './components/CityChooser.vue'
import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-conditions-20241002.json'
import JobSourceDragOrderer from '../../../features/JobSourceDragOrderer/index.vue'
import expectJobFilterTemplateList from './expectJobFilterTemplateList'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
@@ -954,6 +1096,8 @@ const formContent = ref({
dingtalkRobotAccessToken: '',
expectCompanies: '',
anyCombineRecommendJobFilter: {},
combineRecommendJobFilterType: 1,
staticCombineRecommendJobFilterConditions: [],
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '',
expectJobDescRegExpStr: '',
@@ -985,7 +1129,10 @@ const formContent = ref({
type: 'expect',
enabled: true
}
])
]),
isSageTimeEnabled: true,
sageTimeOpTimes: 100,
sageTimePauseMinute: 15
})
const anyCombineBossRecommendFilterHasCondition = computed(() => {
@@ -995,6 +1142,16 @@ const anyCombineBossRecommendFilterHasCondition = computed(() => {
})
const currentAnyCombineRecommendJobFilterCombinationCount = computed(() => {
if (
formContent.value.combineRecommendJobFilterType === CombineRecommendJobFilterType.STATIC_COMBINE
) {
if (!formContent.value.staticCombineRecommendJobFilterConditions?.length) {
return 1
}
return formatStaticCombineFilters(
formContent.value.staticCombineRecommendJobFilterConditions
)?.filter(Boolean).length
}
return calculateTotalCombinations(
formContent.value.anyCombineRecommendJobFilter,
anyCombineBossRecommendFilterHasCondition.value
@@ -1041,6 +1198,12 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
deep: true
}
)
formContent.value.combineRecommendJobFilterType =
res.config['boss.json']?.combineRecommendJobFilterType ?? 1
formContent.value.staticCombineRecommendJobFilterConditions = res.config['boss.json']
?.staticCombineRecommendJobFilterConditions?.length
? res.config['boss.json'].staticCombineRecommendJobFilterConditions
: []
if (
res.config['boss.json']?.expectJobRegExpStr &&
typeof res.config['boss.json']?.expectJobNameRegExpStr === 'undefined' &&
@@ -1116,12 +1279,24 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
formContent.value.__jobSourceList = formatJobSourceConfigToFormValue(
res.config['boss.json'].jobSourceList || []
)
formContent.value.isSageTimeEnabled = res.config['boss.json'].isSageTimeEnabled ?? true
formContent.value.sageTimeOpTimes =
isNaN(parseInt(res.config['boss.json'].sageTimeOpTimes)) ||
parseInt(res.config['boss.json'].sageTimeOpTimes) < 1
? 100
: parseInt(res.config['boss.json'].sageTimeOpTimes)
formContent.value.sageTimePauseMinute =
isNaN(parseFloat(res.config['boss.json'].sageTimePauseMinute)) ||
parseFloat(res.config['boss.json'].sageTimePauseMinute) < 0
? 15
: parseFloat(res.config['boss.json'].sageTimePauseMinute)
})
const jobSourceFormItemSectionEl = ref()
const jobDetailRegExpSectionEl = ref()
const formRules = {
expectJobNameRegExpStr: {
trigger: 'blur',
validator(_, value, cb) {
if (!value) {
cb()
@@ -1130,16 +1305,17 @@ const formRules = {
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_name')
gtagRenderer('valid_reg_exp_for_expect_job_name', { v: value })
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_name')
gtagRenderer('invalid_reg_exp_for_expect_job_name', { v: value })
}
}
},
expectJobTypeRegExpStr: {
trigger: 'blur',
validator(_, value, cb) {
if (!value) {
cb()
@@ -1148,16 +1324,17 @@ const formRules = {
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_type')
gtagRenderer('valid_reg_exp_for_expect_job_type', { v: value })
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_type')
gtagRenderer('invalid_reg_exp_for_expect_job_type', { v: value })
}
}
},
expectJobDescRegExpStr: {
trigger: 'blur',
validator(_, value, cb) {
if (!value) {
cb()
@@ -1166,12 +1343,12 @@ const formRules = {
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_desc')
gtagRenderer('valid_reg_exp_for_expect_job_desc', { v: value })
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_desc')
gtagRenderer('invalid_reg_exp_for_expect_job_desc', { v: value })
}
}
},
@@ -1205,14 +1382,51 @@ const formRules = {
}
cb()
}
},
sageTimeOpTimes: {
validator(_, value, cb) {
if (!formContent.value.isSageTimeEnabled) {
cb()
return
}
if (value < 1 || isNaN(parseInt(value))) {
cb(new Error(`最小值为1请重试`))
return
}
cb()
}
},
sageTimePauseMinute: {
validator(_, value, cb) {
if (!formContent.value.isSageTimeEnabled) {
cb()
return
}
if (value < 0 || isNaN(parseFloat(value))) {
cb(new Error(`最小值为0请重试`))
return
}
cb()
}
}
}
const formRef = ref<InstanceType<typeof ElForm>>()
const handleSubmit = async () => {
gtagRenderer('save_config_and_launch_clicked', {
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken,
expect_job_name_reg_exp_str: formContent.value?.expectJobNameRegExpStr,
expect_job_type_reg_exp_str: formContent.value?.expectJobTypeRegExpStr,
expect_job_desc_reg_exp_str: formContent.value?.expectJobDescRegExpStr,
crjf_type: formContent.value?.combineRecommendJobFilterType,
crjf_cc: currentAnyCombineRecommendJobFilterCombinationCount.value?.toLocaleString?.(),
sage_t_config: JSON.stringify({
isEnabled: formContent.value.isSageTimeEnabled,
pauseMinute: formContent.value.sageTimePauseMinute,
opTimes: formContent.value.sageTimeOpTimes
})
})
// remove the obsolete filter - expectJobRegExpStr
formContent.value.expectJobRegExpStr = undefined
try {
await formRef.value!.validate()
@@ -1241,7 +1455,17 @@ const handleSubmit = async () => {
}
const handleSave = async () => {
gtagRenderer('save_config_clicked', {
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken,
expect_job_name_reg_exp_str: formContent.value?.expectJobNameRegExpStr,
expect_job_type_reg_exp_str: formContent.value?.expectJobTypeRegExpStr,
expect_job_desc_reg_exp_str: formContent.value?.expectJobDescRegExpStr,
crjf_type: formContent.value?.combineRecommendJobFilterType,
crjf_cc: currentAnyCombineRecommendJobFilterCombinationCount.value?.toLocaleString?.(),
sage_t_config: JSON.stringify({
isEnabled: formContent.value.isSageTimeEnabled,
pauseMinute: formContent.value.sageTimePauseMinute,
opTimes: formContent.value.sageTimeOpTimes
})
})
normalizeExpectCompanies()
try {
@@ -1318,53 +1542,6 @@ function handleExpectCompanyTemplateClicked(item) {
formContent.value.expectCompanies = item.value
}
const expectJobFilterTemplateList = [
{
name: '不限职位(随便投)',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
name: '研发 - 前端开发工程师',
config: {
expectJobNameRegExpStr: '前端|H5|FE',
expectJobTypeRegExpStr: '前端开发|javascript',
expectJobDescRegExpStr: '前端|vue|react|node|js|javascript|H5',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
name: '研发 - Java',
config: {
expectJobNameRegExpStr: '\\bJava\\b',
expectJobTypeRegExpStr: '\\bJava\\b',
expectJobDescRegExpStr: '\\bJava\\b|JVM|消息队列|MQ|MySQL|Nginx|Redis|Dubbo',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
name: '人力 - 员工关系',
config: {
expectJobNameRegExpStr: '员工关系|劳动关系|SSC|人力资源|人资',
expectJobTypeRegExpStr: '员工关系|人力资源',
expectJobDescRegExpStr: '社保|考勤|入职|离职',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
name: '人力 - 招聘',
config: {
expectJobNameRegExpStr: '招聘|招聘HR|招聘专员|招聘顾问|招聘专家|Recruiter|人力资源|人资',
expectJobTypeRegExpStr: '招聘|人力资源|猎头顾问',
expectJobDescRegExpStr: '简历筛选|面试安排|offer|猎头',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
}
]
function handleExpectJobFilterTemplateClicked(item) {
gtagRenderer('expect_job_filter_tpl_clicked', {
name: item.name
@@ -1615,6 +1792,17 @@ function formatJobSourceFormValueToConfig(formValue = []) {
return it
})
}
const combineRecommendJobFilterTypeOptions = [
{
name: '使用自由组合条件进行遍历',
value: 1
},
{
name: '使用固定组合条件进行遍历',
value: 2
}
]
</script>
<style scoped lang="scss">

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex h100vh">
<div class="flex flex-col w200px pt30px pl30px aside-nav of-hidden">
<div class="flex flex-col min-w200px w200px pt30px pl30px aside-nav of-hidden">
<div class="nav-list flex-1 of-auto">
<RouterLink to="./GeekAutoStartChatWithBoss">
Boss炸弹