Compare commits

...

39 Commits

Author SHA1 Message Date
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
geekgeekrun
d21972020e ui-v0.9.4 2025-10-14 01:52:07 +08:00
geekgeekrun
51ca5dd71d fix when choose "应届生" in "工作经验", every job is marked as not suit 2025-10-14 01:49:57 +08:00
geekgeekrun
e7d5ef382e ui-v0.9.3 2025-10-13 00:44:31 +08:00
geekgeekrun
59a36dc7bd Merge remote-tracking branch 'origin/master' into feature/ui 2025-10-13 00:43:05 +08:00
geekgeekrun
73b57e21e4 fix tip of daily chat startup chance used out. fix noOpCondition is not take effect. 2025-10-13 00:42:48 +08:00
geekgeekrun
6f0d98653c enhance ui text 2025-10-13 00:39:31 +08:00
geekgeekrun
3bf6d4c358 enhance electron.vite.config.ts 2025-10-04 01:24:18 +08:00
geekgeekrun
2c7ecafee2 fix issue will cause app crash when mark job not suit due to salary; fix salary compare logic 2025-10-04 01:23:01 +08:00
geekgeekrun
f88a5a2889 Merge branch 'feature/ui' 2025-08-17 13:51:09 +08:00
geekgeekrun
ac19ae3832 ui-v0.9.2 2025-08-17 11:16:42 +08:00
geekgeekrun
a25b047f99 fix ci error  ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with packages/ui/package.json 2025-08-17 11:16:14 +08:00
geekgeekrun
42fc66a0f4 fix cannot run due to lodash-es is not bundled; fix logic about set security question tip modal 2025-08-17 11:11:15 +08:00
geekgeekrun
203fbba9a0 ui-v0.9.1 2025-08-17 01:53:49 +08:00
geekgeekrun
b821bc500c fix error for cannot find chooseReasonDialogProxy 2025-08-17 01:50:51 +08:00
geekgeekrun
0032a356e5 fix: reduce argument getBossIdsByJobIds count to fix error: Error: SQLITE_ERROR: Expression tree is too large (maximum depth 1000) 2025-08-17 00:04:05 +08:00
geekgeekrun
b9c4976291 fix: cannot run due to QueryFailedError: SQLITE_ERROR: near ")": syntax error - when one of jobNotMatchStrategy/jobNotActiveStrategy/expectCityNotMatchStrategy set to MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL and chattedJobIds.length === 0, application will crash 2025-08-17 00:00:34 +08:00
geekgeekrun
558891ad9f Merge branch 'master' into feature/ui 2025-08-16 16:38:34 +08:00
geekgeekrun-maintainer
d2da9f5a7d Update README.md 2025-07-31 01:18:29 +08:00
geekgeekrun-maintainer
71068be21f Update README.md 2025-07-25 02:27:45 +08:00
geekgeekrun-maintainer
2b0cf2a2fa Update README.md 2025-07-15 13:44:41 +08:00
geekgeekrun-maintainer
ceaf68e4eb Update README.md 2025-07-15 13:28:39 +08:00
geekgeekrun-maintainer
10cc248784 Update README.md 2025-07-15 13:23:04 +08:00
17 changed files with 953 additions and 230 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

@@ -12,33 +12,34 @@
## 程序有哪些功能?运行逻辑是什么?怎样使用?
### Boss炸弹
扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的Boss。
扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的Boss。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ebf5c51c-f52c-4426-a60a-97916f663c6a" />
<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" />
#### 匹配步骤
1. 按照公司名称查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
2. 检查Boss活跃度
- 如果Boss活跃度为本月活跃或更往前的时间则会把职位标记为不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位
3. 对职位名称、职位类型、职位描述进行匹配
- 如果匹配则自动点击开聊按钮
- 不匹配则标记这个职位为不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位
1. 选择职位来源、职位筛选条件
<img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、Boss活跃度进行匹配
- 如果可以匹配,则自动点击开聊按钮
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
- 首先查看是否命中策略为“在Boss直聘上标记不合适”的配置如果命中则在Boss直聘上点击相关UI标记不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位对于“推荐列表中的职位”、“通过搜索找到的职位”此项会回落到“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”
- 然后查看是否命中策略为“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”的配置如果命中则仅在本地数据库中记录且本次运行将无视这个职位
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
#### 异常情况
- 当前页面筛选条件下,如果没有更多职位,则自动切换备选筛选条件,以获取更多新职位
- 当前页面筛选条件下,如果没有更多可投递职位,则自动切换筛选条件,以获取更多新职位;如果所有筛选条件搜索完没有更多可投递职位,则切换其它职位来源继续查找更多新职位;如果所有职位来源筛选完没有更多可投递职位,则等候一段时间后,会回到第一个职位来源的第一个筛选条件,从头开始查找职位
- 如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
#### 运行效果
> Boss直聘界面已改版如下是旧版Boss直聘界面
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
### 已读不回提醒器
Boss不明原因已读不回简历就是投不出去
@@ -118,12 +119,12 @@ Boss不明原因已读不回简历就是投不出去
```
1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置
## 关于软件不能正常启动、不能按预期运行的一些说明(朋友们帮忙测试一下吧)
## 关于软件不能正常启动、不能按预期运行的一些说明
本程序一直都在不断地更新迭代中包括新功能的增加以及Bug的修复。
然而作者本人只有 1台 macOS Apple Silicon设备、1台 Windows 设备且只有作者1人自测白天工作晚上熬夜开发、测试测试条件不是太好人力不足、设备不足测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时就会准出、发版。
然而作者本人只有 1台 macOS Apple Silicon设备、1台 Windows 设备且只有作者1人自测白天工作晚上熬夜开发、测试测试条件不是太好人力不足、设备不足测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时即认为可以准出、发版。
这将不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看问题也会尝试进行修复。
所以不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看问题也会尝试进行修复。
如果部分朋友超级喜欢写代码,或者本身工作就是写代码,可以帮忙下载 [Debugtron](https://github.com/pd4d10/debugtron/releases/tag/v1.0.0-alpha.0) 来调试本程序,查看反馈本程序的日志(如下图)。
@@ -185,6 +186,13 @@ Boss不明原因已读不回简历就是投不出去
- 请在Boss直聘上自行**屏蔽您不期望投递的公司**如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃请**在Boss直聘上屏蔽当前公司及与之关联的公司**。
- 本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过VMware Workstation/Fusion、Oracle VirtualBox、Microsoft Hyper-V等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
## 想联系作者?想反馈使用体验上的问题?想找求职搭子?
<img width="360" alt="QQ Group QRCode" src="https://github.com/user-attachments/assets/67560948-467b-44f5-a3e3-c8713255d8b7" />
欢迎进群聊天(刚建的群,人有点少,大胆聊就好)
--------
以上

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,

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,
@@ -85,8 +94,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 +119,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
@@ -251,33 +273,43 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
await writeStorageFile('job-not-suit-reason-code-to-text-cache.json', reasonCodeToTextMap)
await sleepWithRandomDelay(2000)
const chooseReasonDialogProxy = await(async() => {
const alls = await page.$$('.zp-dialog-wrap.zp-feedback-dialog.v-transfer-dom')
const alls = await page.$$('.zp-dialog-wrap.zp-feedback-dialog')
return alls?.[alls.length - 1]
})()
let isOptionChosen = false
if (chooseReasonDialogProxy) {
switch (reasonCode) {
case MarkAsNotSuitReason.BOSS_INACTIVE: {
const bossNotActiveOptionProxy = await chooseReasonDialogProxy.$(`.zp-type-item[title="BOSS活跃度低"]`)
if (bossNotActiveOptionProxy) {
await bossNotActiveOptionProxy.click()
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title="BOSS活跃度低"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="职位停招/招满"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
if (opProxy) {
await opProxy.click()
isOptionChosen = true
} else {
const recruitStoppedOptionProxy = await chooseReasonDialogProxy.$(`.zp-type-item[title="职位停招/招满"]`)
if (recruitStoppedOptionProxy) {
await recruitStoppedOptionProxy.click()
isOptionChosen = true
}
}
break
}
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT:
case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: {
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) {
await opProxy.click()
isOptionChosen = true
}
break
}
case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: {
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title*="薪资"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title$="城市"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离远"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="公司"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
if (opProxy) {
await opProxy.click()
isOptionChosen = true
@@ -286,9 +318,11 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
}
case MarkAsNotSuitReason.JOB_NOT_SUIT:
default: {
const jobNotSuitOptionProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="职位"]`)) ?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
if (jobNotSuitOptionProxy) {
await jobNotSuitOptionProxy.click()
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="职位"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
if (opProxy) {
await opProxy.click()
isOptionChosen = true
}
break
@@ -336,7 +370,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
try {
if (expectJobNameRegExpStr.trim()) {
const regExp = new RegExp(expectJobNameRegExpStr, 'i')
isJobNameSuit = regExp.test(jobInfo.jobName)
isJobNameSuit = regExp.test(jobInfo.jobName?.replace(/\n/g, '') ?? '')
}
} catch {
}
@@ -344,7 +378,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
try {
if (expectJobTypeRegExpStr.trim()) {
const regExp = new RegExp(expectJobTypeRegExpStr, 'i')
isJobTypeSuit = regExp.test(jobInfo.positionName)
isJobTypeSuit = regExp.test(jobInfo.positionName?.replace(/\n/g, '') ?? '')
}
} catch {
}
@@ -352,7 +386,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
try {
if (expectJobDescRegExpStr.trim()) {
const regExp = new RegExp(expectJobDescRegExpStr, 'i')
isJobDescSuit = regExp.test(jobInfo.postDescription)
isJobDescSuit = regExp.test(jobInfo.postDescription?.replace(/\n/g, '') ?? '')
}
} catch {
}
@@ -501,16 +535,22 @@ 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')
) {
throw new Error("ACCESS_IS_DENIED")
}
await page.waitForFunction(({ recommendJobPageUrl }) => {
return location.href.startsWith(recommendJobPageUrl) && document.readyState === 'complete'
}, undefined, { recommendJobPageUrl })
hooks.pageLoaded?.call()
let userInfoResponse = await userInfoPromise
@@ -525,20 +565,6 @@ async function toRecommendPage (hooks) {
await storeStorage(page).catch(() => void 0)
}
// check set security question tip modal
let setSecurityQuestionTipModelProxy = await page.$('.dialog-wrap.dialog-account-safe')
if (
setSecurityQuestionTipModelProxy
) {
await sleep(1000)
setSecurityQuestionTipModelProxy = await page.$('.dialog-wrap.dialog-account-safe')
const closeButtonProxy = await setSecurityQuestionTipModelProxy?.$('.close')
if (setSecurityQuestionTipModelProxy && closeButtonProxy) {
await closeButtonProxy.click()
}
}
const computedSourceList = []
for (const source of normalizedJobSource) {
switch (source.type) {
@@ -628,11 +654,33 @@ async function toRecommendPage (hooks) {
let currentSourceIndex = 0
afterPageLoad: while (true) {
// check set security question tip modal
let setSecurityQuestionTipModelProxy
try {
setSecurityQuestionTipModelProxy = await page.waitForSelector('.dialog-wrap.dialog-account-safe', { timeout: 3 * 1000 })
}
catch(err) {
console.log(`cannot find set security question tip modal, just continue`)
}
if (
setSecurityQuestionTipModelProxy
) {
await sleep(1000)
setSecurityQuestionTipModelProxy = await page.$('.dialog-wrap.dialog-account-safe')
const closeButtonProxy = await setSecurityQuestionTipModelProxy?.$('.close')
if (setSecurityQuestionTipModelProxy && closeButtonProxy) {
await closeButtonProxy.click()
}
}
const filterConditions =
combineRecommendJobFilterType === CombineRecommendJobFilterType.STATIC_COMBINE
? formatStaticCombineFilters(staticCombineRecommendJobFilterConditions)
: combineFiltersWithConstraintsGenerator(anyCombineRecommendJobFilter)
let expectJobList
iterateFilterCondition: for (
const filterCondition of combineFiltersWithConstraintsGenerator(
anyCombineRecommendJobFilter
)
const filterCondition of filterConditions
) {
findInCurrentFilterCondition: while(true) {
await sleepWithRandomDelay(2500)
@@ -657,11 +705,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
@@ -784,7 +838,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) {
@@ -792,7 +846,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
}
}
@@ -962,7 +1016,8 @@ async function toRecommendPage (hooks) {
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
} catch(err) {
console.log(`mark boss inactive failed`, err)
}
}
},
@@ -998,7 +1053,8 @@ async function toRecommendPage (hooks) {
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
} catch(err) {
console.log(`mark job city not suit failed`, err)
}
}
},
@@ -1034,7 +1090,8 @@ async function toRecommendPage (hooks) {
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
} catch(err) {
console.log(`mark job work exp not suit failed`, err)
}
}
},
@@ -1071,7 +1128,8 @@ async function toRecommendPage (hooks) {
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
} catch(err) {
console.log(`mark job detail not suit failed`, err)
}
}
},
@@ -1110,7 +1168,8 @@ async function toRecommendPage (hooks) {
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
} catch(err) {
console.log(`mark job salary not suit failed`, err)
}
}
}
@@ -1165,6 +1224,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
@@ -1215,28 +1280,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,
{
@@ -1252,6 +1310,50 @@ 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)
) {
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)
) {
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

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

View File

@@ -1,7 +1,6 @@
import "reflect-metadata";
import { type DataSource } from "typeorm";
import { requireTypeorm } from "./utils/module-loader";
import fs from 'node:fs'
import { BossInfo } from "./entity/BossInfo";
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
@@ -38,6 +37,7 @@ import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
import { Init1000000000000 } from "./migrations/1000000000000-Init";
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
const lodashImportPromise = import('lodash-es')
export function initDb(dbFilePath) {
const { DataSource } = requireTypeorm()
@@ -175,7 +175,16 @@ export default class SqlitePlugin {
}
const last30DayChatStartupRecords = (await getChatStartupRecordsInLastSomeDays(ds, 30)) ?? [];
const chattedJobIds = last30DayChatStartupRecords.map(it => it.encryptJobId)
const chattedBossIds = ((await getBossIdsByJobIds(ds, chattedJobIds)) ?? []).map(it => it.encryptBossId)
if (chattedJobIds.length === 0) {
return
}
const { chunk } = await lodashImportPromise
const chattedJobIdChunks = chunk(chattedJobIds, 200)
const chattedBossIds = [];
for (const chattedJobIdChunk of chattedJobIdChunks) {
const chattedBossIdChunk = ((await getBossIdsByJobIds(ds, chattedJobIdChunk)) ?? []).map(it => it.encryptBossId)
chattedBossIds.push(...chattedBossIdChunk)
}
for (const id of chattedBossIds) {
blockBossNotNewChat.add(id)
}

View File

@@ -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: /<measurement_id>/g,
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string,
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string
},
{
find: /<api_secret>/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'
}
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "geekgeekrun-ui",
"version": "0.9.0",
"version": "0.10.2",
"description": "Boss 炸弹 - 自动开聊Boss助力每位打工人求职",
"main": "./out/main/index.js",
"author": "geekgeekrun",
@@ -38,6 +38,7 @@
"JSONStream": "^1.3.5",
"diff": "^7.0.0",
"electron-updater": "^6.1.7",
"lodash-es": "^4.17.21",
"minimist": "^1.2.8",
"mitt": "^3.0.1",
"node-machine-id": "^1.1.12",
@@ -72,7 +73,6 @@
"eslint-plugin-vue": "^9.20.1",
"find-chrome-bin": "^2.0.1",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"normalize.css": "^8.0.1",
"prettier": "^3.2.4",
"sass": "^1.70.0",

View File

@@ -1,7 +1,7 @@
{
"version": "0.9.0",
"buildVersion": 16,
"buildTime": 1752412320912,
"buildHash": "d3e086f32184dd2a36819c0f9697228fa1316ada",
"version": "0.10.2",
"buildVersion": 24,
"buildTime": 1765548632185,
"buildHash": "056ec7a0a6c3225829dff3da36712fb38ab7635f",
"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,
@@ -121,6 +121,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,13 @@ 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
}
promiseArr.push(writeConfigFile('boss.json', bossConfig))

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

@@ -22,7 +22,7 @@
<div w-full>
<div ref="jobSourceFormItemSectionEl" font-size-16px>
<div>
职位来源及其查找顺序
你想投递Boss直聘上哪些列表里的职位
<el-tooltip
effect="light"
placement="bottom-start"
@@ -41,7 +41,7 @@
</div>
</div>
<div font-size-12px>
拖放条目前方的手柄以调整职位来源查找顺序点击条目前方的开关以启用/禁用对应的职位来源
拖放条目前方的手柄以调整职位列表查找顺序点击条目前方的开关以启用/禁用对应的职位列表
</div>
<JobSourceDragOrderer
v-model="formContent.__jobSourceList"
@@ -49,62 +49,99 @@
/>
</div>
</el-form-item>
</el-card>
<el-card class="config-section">
<el-form-item prop="filter">
<div font-size-16px>
职位筛选条件
<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用于记录开聊请勿使用公司内部群"
@@ -582,9 +619,7 @@
</div>
</div>
</div>
</el-card>
<el-card class="config-section">
<el-form-item mb0>
<el-form-item mb0 mt20px>
<div font-size-16px>职位详情筛选条件</div>
</el-form-item>
<div>
@@ -617,7 +652,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"
@@ -925,15 +965,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,
@@ -944,6 +987,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, {
@@ -958,6 +1002,8 @@ const formContent = ref({
dingtalkRobotAccessToken: '',
expectCompanies: '',
anyCombineRecommendJobFilter: {},
combineRecommendJobFilterType: 1,
staticCombineRecommendJobFilterConditions: [],
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '',
expectJobDescRegExpStr: '',
@@ -999,6 +1045,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
@@ -1045,6 +1101,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' &&
@@ -1126,6 +1188,7 @@ const jobSourceFormItemSectionEl = ref()
const jobDetailRegExpSectionEl = ref()
const formRules = {
expectJobNameRegExpStr: {
trigger: 'blur',
validator(_, value, cb) {
if (!value) {
cb()
@@ -1134,16 +1197,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()
@@ -1152,16 +1216,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()
@@ -1170,12 +1235,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 })
}
}
},
@@ -1215,8 +1280,14 @@ const formRules = {
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?.()
})
// remove the obsolete filter - expectJobRegExpStr
formContent.value.expectJobRegExpStr = undefined
try {
await formRef.value!.validate()
@@ -1322,53 +1393,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
@@ -1619,6 +1643,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炸弹

7
pnpm-lock.yaml generated
View File

@@ -129,6 +129,9 @@ importers:
electron-updater:
specifier: ^6.1.7
version: 6.1.7
lodash-es:
specifier: ^4.17.21
version: 4.17.21
minimist:
specifier: ^1.2.8
version: 1.2.8
@@ -226,9 +229,6 @@ importers:
js-yaml:
specifier: ^4.1.0
version: 4.1.0
lodash-es:
specifier: ^4.17.21
version: 4.17.21
normalize.css:
specifier: ^8.0.1
version: 8.0.1
@@ -4570,7 +4570,6 @@ packages:
/lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: true
/lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}