mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-07-12 16:01:38 +08:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d155e498a | ||
|
|
d3e086f321 | ||
|
|
5f97b3ccec | ||
|
|
7dd0c675d5 | ||
|
|
c1978417da | ||
|
|
e0670a9f30 | ||
|
|
b05031be8a | ||
|
|
7b10e97fa8 | ||
|
|
e867acb39d | ||
|
|
b31224a81c | ||
|
|
ddb0cb97d7 | ||
|
|
f338dcffc8 | ||
|
|
84a79c9bae | ||
|
|
44b0e44e2e | ||
|
|
08028f37dd | ||
|
|
a0b09ebae4 | ||
|
|
f078500c14 | ||
|
|
b89d6e264d | ||
|
|
78ddc949f4 | ||
|
|
a208ffe7d4 | ||
|
|
bc70031e2b | ||
|
|
cc821f46f5 | ||
|
|
f668b3e5f5 | ||
|
|
d1441b5761 | ||
|
|
e88b4da53a | ||
|
|
d218778690 | ||
|
|
f903ef9175 | ||
|
|
d114685116 | ||
|
|
bb30329d6c | ||
|
|
3683ead1d4 | ||
|
|
cac3e480e5 | ||
|
|
52163342c3 | ||
|
|
8efce15fd5 | ||
|
|
d5dcdb4bd1 | ||
|
|
98b99c5d14 | ||
|
|
d4fa72a28e | ||
|
|
b024c6b517 | ||
|
|
9b64f05cb6 | ||
|
|
eefb25a3da | ||
|
|
2dad39c8e4 | ||
|
|
699bdd2641 | ||
|
|
48f1232f7d | ||
|
|
2409699ccf | ||
|
|
64c007066e | ||
|
|
bd503eb268 | ||
|
|
8dc8ca825d | ||
|
|
8e8fb6d331 | ||
|
|
2f3a557fb5 | ||
|
|
780cd73e46 | ||
|
|
6c05128e89 |
36
README.md
36
README.md
@@ -118,6 +118,19 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
```
|
||||
1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置
|
||||
|
||||
## 关于软件不能正常启动、不能按预期运行的一些说明(朋友们帮忙测试一下吧)
|
||||
本程序一直都在不断地更新迭代中,包括新功能的增加以及Bug的修复。
|
||||
|
||||
然而作者本人只有 1台 macOS (Apple Silicon)设备、1台 Windows 设备,且只有作者1人自测(白天工作,晚上熬夜开发、测试),测试条件不是太好(人力不足、设备不足),测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时,就会准出、发版。
|
||||
|
||||
这将不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看,问题也会尝试进行修复。
|
||||
|
||||
如果部分朋友超级喜欢写代码,或者本身工作就是写代码,可以帮忙下载 [Debugtron](https://github.com/pd4d10/debugtron/releases/tag/v1.0.0-alpha.0) 来调试本程序,查看反馈本程序的日志(如下图)。
|
||||
|
||||

|
||||
|
||||
在此先谢过了~
|
||||
|
||||
## 为什么要写这个程序?
|
||||
### 个人而言
|
||||
以我为例,我真的求职求累了
|
||||
@@ -143,8 +156,8 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场
|
||||
|
||||
当然,如果运气好:
|
||||
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个烂 Offer
|
||||
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了烂 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
|
||||
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个不满意的 Offer
|
||||
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了不满意的 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
|
||||
|
||||
为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧
|
||||
|
||||
@@ -157,7 +170,20 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
|
||||
人工筛选这些职位会有很大的心智负担,真的会吐……
|
||||
|
||||
因此,我在Boss炸弹代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位
|
||||
因此,我在Boss炸弹代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位。
|
||||
|
||||
## 使用必读及免责声明
|
||||
如下是使用必读及免责声明,请您务必逐条阅读;本程序首次启动时,您还会再看到一次;若您不接受如下提到的任何内容,请不要使用本程序。
|
||||
- 本程序从某种程度上说属于辅助工具,与《<a href="https://about.zhipin.com/agreement/?id=registerprotocol_30" rel="noreferer noopener" target="blank">Boss直聘用户协议</a>》(2023年3月版)相关条款相违背,您在注册Boss直聘时已签署过这一条款;根据该条款`七、用户的平台使用义务`、`八、违约责任` 章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于**账号被强制退出登录**、**账号被限制使用**、**账号被封禁**等对您不利的风险;因此使用本程序即意味着**您愿意接受以上风险**,且如果相关风险发生,您需要自行承担相关后果,**本程序概不负责** 。
|
||||
- 本程序需要存储您的登录凭据,即Cookie,来模拟您在Boss直聘上开聊Boss的行为;本程序仅会把您的Cookie存储在本地,并在您访问Boss直聘时将其传输到Boss直聘,**不会泄露给第三方**,也不会进行除自动开聊Boss以外的行为;**请勿向他人泄漏您的Cookie**。
|
||||
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的Boss直聘账号**进行求职。
|
||||
- 本程序原理是模拟用户在Boss直聘网页上,寻找关键元素并进行点击操作;Boss直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
||||
- 您所在公司可能会对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;上级/HRBP团队可能会从 IT 团队处获取到监控数据,从而了解团队成员离职倾向。如果您不希望上级/HRBP团队了解到您正在求职,建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
|
||||
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及上级/HRBP报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**。
|
||||
- 本程序**没有内置任何付费功能**,**下载**、**使用**是**免费**的,任何人可以**免费获得**、**免费使用**。**作者没有利用本程序赚到过任何收入**。如果您是从GitHub以外的地方付费后“购买”的本程序,或您被提示“必须付费后才能使用本程序”,那**您大概率被骗了**,或者**您下载到了本程序修改版**。**本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方**。
|
||||
- 本程序**不对您的求职过程与结果负责**,为您开聊的职位均在Boss直聘上发布,职位信息真实性由Boss直聘负责;请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
|
||||
- 请在Boss直聘上自行**屏蔽您不期望投递的公司**;如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃,请**在Boss直聘上屏蔽当前公司及与之关联的公司**。
|
||||
- 本程序经历过了多次测试,理论上来说大部分情况下可以正常运行,但可能也会出现测试用例覆盖不到位,导致程序不按预期运行的情况;如果您有顾虑,建议通过VMware Workstation/Fusion、Oracle VirtualBox、Microsoft Hyper-V等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
||||
|
||||
--------
|
||||
|
||||
@@ -167,10 +193,12 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
|
||||
祝你求职成功,事业顺利,事事顺心
|
||||
|
||||
## Star 数据 感谢支持
|
||||
## Star
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date" />
|
||||
</picture>
|
||||
|
||||
感谢支持
|
||||
|
||||
@@ -90,7 +90,7 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
//#region get count of combinations
|
||||
|
||||
// 计算符合限制条件的组合数量
|
||||
export function calculateTotalCombinations(selectedFilters) {
|
||||
export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) {
|
||||
const {
|
||||
salaryList = [],
|
||||
experienceList = [],
|
||||
@@ -106,8 +106,21 @@ export function calculateTotalCombinations(selectedFilters) {
|
||||
const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多
|
||||
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
|
||||
|
||||
return [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
|
||||
let result = [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
|
||||
return accu * cur.length
|
||||
}, 1)
|
||||
if (!includeEmptyCondition) {
|
||||
result -= 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
//#endregion
|
||||
|
||||
export function checkAnyCombineBossRecommendFilterHasCondition(value) {
|
||||
if (!Object.keys(value ?? {}).length) {
|
||||
return false
|
||||
}
|
||||
return Object.keys(value).some((k) => {
|
||||
return !!value[k]?.length
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export const activeDescList = [
|
||||
'', "半年前活跃","近半年活跃","5月内活跃","4月内活跃","3月内活跃","2月内活跃","本月活跃","2周内活跃","本周活跃","3日内活跃","昨日活跃","今日活跃","刚刚活跃"
|
||||
]
|
||||
]
|
||||
|
||||
export const RECOMMEND_JOB_ENTRY_SELECTOR = `.c-expect-select a[ka="jobs_recommend_tab_click"]`
|
||||
export const USER_SET_EXPECT_JOB_ENTRIES_SELECTOR = `.c-expect-select .expect-list .expect-item`
|
||||
export const SEARCH_BOX_SELECTOR = `.c-search-input .search-input-box`
|
||||
@@ -6,6 +6,7 @@
|
||||
"scaleList": [],
|
||||
"industryList": []
|
||||
},
|
||||
"isSkipEmptyConditionForCombineRecommendJobFilter": false,
|
||||
"expectJobRegExpStr": "",
|
||||
"jobNotMatchStrategy": 1,
|
||||
"jobNotActiveStrategy": 1,
|
||||
@@ -13,6 +14,7 @@
|
||||
"expectCityList": [],
|
||||
"expectCityNotMatchStrategy": 3,
|
||||
"strategyScopeOptionWhenMarkJobCityNotMatch": 2,
|
||||
"jobDetailRegExpMatchLogic": 1,
|
||||
"autoReminder": {
|
||||
"throttleIntervalMinutes": 10,
|
||||
"rechatLimitDay": 21,
|
||||
@@ -21,5 +23,11 @@
|
||||
"recentMessageQuantityForLlm": 8,
|
||||
"rechatLlmFallback": 1,
|
||||
"onlyRemindBossWithExpectJobType": true
|
||||
}
|
||||
},
|
||||
"jobSourceList": [
|
||||
{
|
||||
"type": "expect",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,13 +12,22 @@ import { EventEmitter } from 'node:events'
|
||||
import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
|
||||
|
||||
import { readConfigFile, writeStorageFile, ensureConfigFileExist, readStorageFile, ensureStorageFileExist } from './runtime-file-utils.mjs'
|
||||
import { calculateTotalCombinations, combineFiltersWithConstraintsGenerator } from './combineCalculator.mjs'
|
||||
import {
|
||||
calculateTotalCombinations,
|
||||
combineFiltersWithConstraintsGenerator,
|
||||
checkAnyCombineBossRecommendFilterHasCondition
|
||||
} from './combineCalculator.mjs'
|
||||
import { default as jobFilterConditions } from './internal-config/job-filter-conditions-20241002.json'
|
||||
import { default as rawIndustryFilterExemption } from './internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||||
import { ChatStartupFrom } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatStartupLog'
|
||||
import { MarkAsNotSuitReason, MarkAsNotSuitOp, StrategyScopeOptionWhenMarkJobNotMatch } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import { activeDescList } from './constant.mjs'
|
||||
|
||||
import { MarkAsNotSuitReason, MarkAsNotSuitOp, StrategyScopeOptionWhenMarkJobNotMatch, SalaryCalculateWay, JobDetailRegExpMatchLogic, JobSource } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import {
|
||||
activeDescList,
|
||||
RECOMMEND_JOB_ENTRY_SELECTOR,
|
||||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
|
||||
SEARCH_BOX_SELECTOR,
|
||||
} from './constant.mjs'
|
||||
import { parseSalary } from "@geekgeekrun/sqlite-plugin/dist/utils/parser"
|
||||
const jobFilterConditionsMapByCode = {}
|
||||
Object.values(jobFilterConditions).forEach(arr => {
|
||||
arr.forEach(option => {
|
||||
@@ -78,6 +87,10 @@ const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||||
const targetCompanyList = readConfigFile('target-company-list.json').filter(it => !!it.trim());
|
||||
|
||||
const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter
|
||||
let isSkipEmptyConditionForCombineRecommendJobFilter = readConfigFile('boss.json').isSkipEmptyConditionForCombineRecommendJobFilter
|
||||
if (!checkAnyCombineBossRecommendFilterHasCondition(anyCombineRecommendJobFilter)) {
|
||||
isSkipEmptyConditionForCombineRecommendJobFilter = false
|
||||
}
|
||||
const expectJobRegExpStr = readConfigFile('boss.json').expectJobRegExpStr
|
||||
const jobNotMatchStrategy = readConfigFile('boss.json').jobNotMatchStrategy ?? MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
|
||||
@@ -86,6 +99,21 @@ const expectCityList = readConfigFile('boss.json').expectCityList ?? []
|
||||
|
||||
const strategyScopeOptionWhenMarkJobCityNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkJobCityNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
|
||||
|
||||
// salary
|
||||
const expectSalaryLow = parseFloat(readConfigFile('boss.json').expectSalaryLow) || null
|
||||
const expectSalaryHigh = parseFloat(readConfigFile('boss.json').expectSalaryHigh) || null
|
||||
const expectSalaryCalculateWay = readConfigFile('boss.json').expectSalaryCalculateWay ?? SalaryCalculateWay.MONTH_SALARY
|
||||
const expectSalaryNotMatchStrategy = readConfigFile('boss.json').expectSalaryNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
|
||||
const isSalaryFilterEnabled = expectSalaryLow || expectSalaryHigh
|
||||
const strategyScopeOptionWhenMarkSalaryNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkSalaryNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
|
||||
|
||||
// work exp
|
||||
const expectWorkExpList = readConfigFile('boss.json').expectWorkExpList ?? []
|
||||
const expectWorkExpNotMatchStrategy = readConfigFile('boss.json').expectWorkExpNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
|
||||
const strategyScopeOptionWhenMarkJobWorkExpNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkJobWorkExpNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
|
||||
|
||||
let jobDetailRegExpMatchLogic = readConfigFile('boss.json').jobDetailRegExpMatchLogic ?? JobDetailRegExpMatchLogic.EVERY
|
||||
|
||||
const markAsNotActiveSelectedTimeRange = (() => {
|
||||
let n = readConfigFile('boss.json').markAsNotActiveSelectedTimeRange
|
||||
if (
|
||||
@@ -119,6 +147,60 @@ if (
|
||||
expectJobDescRegExpStr = expectJobRegExpStr
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
expectJobNameRegExpStr,
|
||||
expectJobTypeRegExpStr,
|
||||
expectJobDescRegExpStr,
|
||||
].map(it => Boolean(it?.trim())).every(it => !it)
|
||||
) {
|
||||
jobDetailRegExpMatchLogic = JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
|
||||
let {
|
||||
jobSourceList
|
||||
} = readConfigFile('boss.json')
|
||||
const normalizedJobSource = []
|
||||
const addedSourceSet = new Set()
|
||||
for (const source of (jobSourceList ?? [])) {
|
||||
if (addedSourceSet.has(source.type)) {
|
||||
continue
|
||||
}
|
||||
if (!source?.enabled) {
|
||||
continue
|
||||
}
|
||||
if (source.type === 'search') {
|
||||
for (const searchOption of (source.children ?? [])) {
|
||||
if (!searchOption.enabled || !searchOption.keyword?.trim()) {
|
||||
continue
|
||||
}
|
||||
const key = [
|
||||
source.type,
|
||||
searchOption.keyword.trim()
|
||||
].join('__')
|
||||
if (addedSourceSet.has(key)) {
|
||||
continue
|
||||
}
|
||||
normalizedJobSource.push({
|
||||
type: 'search',
|
||||
keyword: searchOption.keyword.trim()
|
||||
})
|
||||
addedSourceSet.add(key)
|
||||
}
|
||||
addedSourceSet.add(source.type)
|
||||
}
|
||||
else {
|
||||
normalizedJobSource.push({
|
||||
type: source.type,
|
||||
})
|
||||
addedSourceSet.add(source.type)
|
||||
}
|
||||
}
|
||||
if (!normalizedJobSource?.length) {
|
||||
normalizedJobSource.push({
|
||||
type: 'expect'
|
||||
})
|
||||
}
|
||||
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
|
||||
const recommendJobPageUrl = `https://www.zhipin.com/web/geek/jobs`
|
||||
|
||||
@@ -189,19 +271,16 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT: {
|
||||
const bossNotActiveOptionProxy = await chooseReasonDialogProxy.$(`.zp-type-item[title$="城市"]`)
|
||||
if (bossNotActiveOptionProxy) {
|
||||
await bossNotActiveOptionProxy.click()
|
||||
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
|
||||
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT:
|
||||
case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: {
|
||||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="城市"]`))
|
||||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="同城距离远"]`))
|
||||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="公司不感兴趣"]`))
|
||||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
|
||||
if (opProxy) {
|
||||
await opProxy.click()
|
||||
isOptionChosen = true
|
||||
} else {
|
||||
const fallbackOptionProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title="同城距离远"]`))
|
||||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="公司不感兴趣"]`))
|
||||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
|
||||
if (fallbackOptionProxy) {
|
||||
await fallbackOptionProxy.click()
|
||||
isOptionChosen = true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -252,8 +331,8 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
||||
return result
|
||||
}
|
||||
|
||||
export function testIfJobTitleOrDescriptionSuit (jobInfo) {
|
||||
let isJobNameSuit = true
|
||||
export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
|
||||
let isJobNameSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||||
try {
|
||||
if (expectJobNameRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobNameRegExpStr, 'i')
|
||||
@@ -261,7 +340,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) {
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
let isJobTypeSuit = true
|
||||
let isJobTypeSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||||
try {
|
||||
if (expectJobTypeRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobTypeRegExpStr, 'i')
|
||||
@@ -269,7 +348,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) {
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
let isJobDescSuit = true
|
||||
let isJobDescSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||||
try {
|
||||
if (expectJobDescRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobDescRegExpStr, 'i')
|
||||
@@ -277,7 +356,12 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) {
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return isJobNameSuit && isJobTypeSuit && isJobDescSuit
|
||||
if (matchLogic === JobDetailRegExpMatchLogic.SOME) {
|
||||
return isJobNameSuit || isJobTypeSuit || isJobDescSuit
|
||||
}
|
||||
else {
|
||||
return isJobNameSuit && isJobTypeSuit && isJobDescSuit
|
||||
}
|
||||
}
|
||||
|
||||
async function setFilterCondition (selectedFilters) {
|
||||
@@ -455,8 +539,94 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
}
|
||||
|
||||
const INIT_START_EXCEPT_JOB_INDEX = 0
|
||||
let currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||||
const computedSourceList = []
|
||||
for (const source of normalizedJobSource) {
|
||||
switch (source.type) {
|
||||
case 'recommend': {
|
||||
computedSourceList.push({
|
||||
type: source.type,
|
||||
selector: RECOMMEND_JOB_ENTRY_SELECTOR,
|
||||
async getIsCurrentActiveSource () {
|
||||
return await page.evaluate(
|
||||
({ RECOMMEND_JOB_ENTRY_SELECTOR }) => {
|
||||
return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active')
|
||||
}, {
|
||||
RECOMMEND_JOB_ENTRY_SELECTOR
|
||||
}
|
||||
)
|
||||
},
|
||||
async setToActiveSource() {
|
||||
// not first navigation and should choose a job (except job)
|
||||
// click first expect job
|
||||
const expectJobTabHandler = await page.$(RECOMMEND_JOB_ENTRY_SELECTOR)
|
||||
await expectJobTabHandler.click() // switch to first condition
|
||||
}
|
||||
})
|
||||
continue
|
||||
}
|
||||
case 'expect': {
|
||||
await page.waitForSelector(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
|
||||
const allExpectJobEntryHandles = await page.$$(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
|
||||
allExpectJobEntryHandles.forEach((it, index) => {
|
||||
computedSourceList.push({
|
||||
type: source.type,
|
||||
selector: `${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`,
|
||||
async getIsCurrentActiveSource () {
|
||||
return await page.evaluate(
|
||||
({
|
||||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
|
||||
index
|
||||
}) => {
|
||||
return document.querySelector(`${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`).classList.contains('active')
|
||||
}, {
|
||||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
|
||||
index
|
||||
}
|
||||
)
|
||||
},
|
||||
async setToActiveSource() {
|
||||
// not first navigation and should choose a job (except job)
|
||||
// click first expect job
|
||||
const expectJobTabHandler = await page.$(`${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`)
|
||||
await expectJobTabHandler.click() // switch to first condition
|
||||
}
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'search': {
|
||||
computedSourceList.push({
|
||||
type: source.type,
|
||||
async getIsCurrentActiveSource () {
|
||||
const elHandle = await page.$(`.page-jobs-main`)
|
||||
const currentKeyWord = await elHandle?.evaluate((el) => {
|
||||
return el?.__vue__?.formData?.query
|
||||
})
|
||||
if (!currentKeyWord) {
|
||||
return false
|
||||
}
|
||||
return currentKeyWord === source.keyword
|
||||
},
|
||||
async setToActiveSource() {
|
||||
await page.waitForSelector(SEARCH_BOX_SELECTOR)
|
||||
const inputHandle = await page.$(`${SEARCH_BOX_SELECTOR} input`)
|
||||
await inputHandle.focus()
|
||||
await sleep(100)
|
||||
let currentValue = await inputHandle.evaluate(el => el.value)
|
||||
while (currentValue) {
|
||||
await inputHandle.press('Backspace')
|
||||
currentValue = await inputHandle.evaluate(el => el.value)
|
||||
}
|
||||
await inputHandle.type(source.keyword?.trim() || '', { delay: 100 })
|
||||
await sleep(500)
|
||||
await inputHandle.press('Enter')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let currentSourceIndex = 0
|
||||
afterPageLoad: while (true) {
|
||||
let expectJobList
|
||||
iterateFilterCondition: for (
|
||||
@@ -468,26 +638,44 @@ async function toRecommendPage (hooks) {
|
||||
await sleepWithRandomDelay(2500)
|
||||
|
||||
await Promise.all([
|
||||
page.waitForSelector('.c-expect-select .expect-list .expect-item'),
|
||||
page.waitForSelector('.job-list-container .rec-job-list')
|
||||
Promise.race([
|
||||
page.waitForSelector(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR),
|
||||
page.waitForSelector(RECOMMEND_JOB_ENTRY_SELECTOR),
|
||||
]),
|
||||
Promise.race([
|
||||
page.waitForSelector(".job-list-container .rec-job-list"),
|
||||
page.waitForSelector(".recommend-result-job .job-empty-wrapper")
|
||||
])
|
||||
])
|
||||
await page.click(`.c-expect-select .expect-list .expect-item`)
|
||||
const currentActiveJobIndex = await page.evaluate(`
|
||||
[...document.querySelectorAll('.c-expect-select .expect-list .expect-item')].findIndex(it => it.classList.contains('active'))
|
||||
`)
|
||||
// await page.click(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
|
||||
await sleep(3000)
|
||||
let onPageCurrentSourceIndex = -1
|
||||
for (let i=0; i < computedSourceList.length; i++) {
|
||||
const computedSource = computedSourceList[i]
|
||||
if (await computedSource.getIsCurrentActiveSource()) {
|
||||
onPageCurrentSourceIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isSkipEmptyConditionForCombineRecommendJobFilter &&
|
||||
Object.keys(filterCondition).length &&
|
||||
Object.keys(filterCondition).every(k => !filterCondition[k]?.length)
|
||||
) {
|
||||
sleep(4000)
|
||||
continue iterateFilterCondition
|
||||
}
|
||||
expectJobList = await page.evaluate(`document.querySelector('.c-expect-select')?.__vue__?.expectList`)
|
||||
if (currentActiveJobIndex === currentExceptJobIndex) {
|
||||
if (onPageCurrentSourceIndex === currentSourceIndex) {
|
||||
// first navigation and can immediately start chat (recommend job)
|
||||
} else {
|
||||
// not first navigation and should choose a job (except job)
|
||||
// click first expect job
|
||||
const expectJobTabHandlers = await page.$$('.c-expect-select .expect-list .expect-item')
|
||||
await expectJobTabHandlers[currentExceptJobIndex].click()
|
||||
await computedSourceList[currentSourceIndex].setToActiveSource()
|
||||
await page.waitForResponse(
|
||||
response => {
|
||||
if (
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json')
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json') ||
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/search/joblist.json')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
@@ -497,9 +685,12 @@ async function toRecommendPage (hooks) {
|
||||
await storeStorage(page).catch(() => void 0)
|
||||
await sleepWithRandomDelay(2000)
|
||||
}
|
||||
await sleepWithRandomDelay(3000)
|
||||
await sleepWithRandomDelay(1500)
|
||||
await setFilterCondition(filterCondition)
|
||||
|
||||
await sleep(1500) // TODO: accurately check if job list request sent and response received after set condition
|
||||
await page.waitForFunction(() => {
|
||||
return !document.querySelector('.job-recommend-result .job-rec-loading')
|
||||
})
|
||||
try {
|
||||
const { targetJobIndex, targetJobData } = await new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
@@ -507,7 +698,10 @@ async function toRecommendPage (hooks) {
|
||||
page.on(
|
||||
'request',
|
||||
function reqHandler (request) {
|
||||
if (request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json')) {
|
||||
if (
|
||||
request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json') ||
|
||||
request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/search/joblist.json')
|
||||
) {
|
||||
requestNextPagePromiseWithResolver = (() => {
|
||||
const o = {}
|
||||
o.promise = new Promise((resolve, reject) => {
|
||||
@@ -530,19 +724,46 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// job list
|
||||
const recommendJobListElProxy = await page.$('.job-list-container .rec-job-list')
|
||||
let recommendJobListElProxy
|
||||
try {
|
||||
recommendJobListElProxy= await page.waitForSelector('.job-list-container .rec-job-list', { timeout: 5 * 1000 })
|
||||
} catch {}
|
||||
if (!recommendJobListElProxy){
|
||||
await hooks.encounterEmptyRecommendJobList?.promise({
|
||||
pageQuery: await page.evaluate(() => new URL(location.href).searchParams.toString())
|
||||
})
|
||||
throw new Error('CANNOT_FIND_EXCEPT_JOB_IN_THIS_FILTER_CONDITION')
|
||||
}
|
||||
let jobListData = []
|
||||
async function updateJobListData () {
|
||||
jobListData = await page.evaluate(`document.querySelector('.page-jobs-main')?.__vue__?.jobList`)
|
||||
// due to city can get from list immediately
|
||||
// so just set those job which city is not suit to blockJobNotSuit
|
||||
// to skip view detail
|
||||
|
||||
// skip invalid salaryData (兼职、日结、实习 etc)
|
||||
jobListData.forEach(it => {
|
||||
const salaryData = parseSalary(it.salaryDesc)
|
||||
if (!salaryData.high || !salaryData.low) {
|
||||
blockJobNotSuit.add(it.encryptJobId)
|
||||
}
|
||||
})
|
||||
if (
|
||||
expectCityNotMatchStrategy === MarkAsNotSuitOp.NO_OP &&
|
||||
Array.isArray(expectCityList) &&
|
||||
expectCityList.length
|
||||
(
|
||||
expectCityNotMatchStrategy === MarkAsNotSuitOp.NO_OP &&
|
||||
Array.isArray(expectCityList) &&
|
||||
expectCityList.length
|
||||
) ||
|
||||
(
|
||||
expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.NO_OP &&
|
||||
Array.isArray(expectWorkExpList) &&
|
||||
expectWorkExpList.length
|
||||
) ||
|
||||
(
|
||||
strategyScopeOptionWhenMarkSalaryNotMatch === MarkAsNotSuitOp.NO_OP &&
|
||||
isSalaryFilterEnabled
|
||||
)
|
||||
) {
|
||||
console.log(`add job city not suit into blockJobNotSuit set`)
|
||||
for (const it of jobListData) {
|
||||
@@ -557,6 +778,26 @@ async function toRecommendPage (hooks) {
|
||||
let hasReachLastPage = false
|
||||
let targetJobIndex = -1
|
||||
let targetJobData, selectedJobData // they show be same; one is from list, another is from detail
|
||||
function checkIfSalarySuit(salaryDesc) {
|
||||
const salaryData = parseSalary(salaryDesc)
|
||||
if (expectSalaryCalculateWay === SalaryCalculateWay.MONTH_SALARY) {
|
||||
if (expectSalaryHigh && salaryData.high > expectSalaryHigh) {
|
||||
return false
|
||||
}
|
||||
if (expectSalaryLow && salaryData.low < expectSalaryLow) {
|
||||
return false
|
||||
}
|
||||
} else if (expectSalaryCalculateWay === SalaryCalculateWay.ANNUAL_PACKAGE) {
|
||||
const salaryDataMonth = salaryData.month || 12
|
||||
if (expectSalaryHigh && (salaryData.high * salaryDataMonth) / 10 > expectSalaryHigh) {
|
||||
return false
|
||||
}
|
||||
if (expectSalaryLow && (salaryData.low * salaryDataMonth) / 10 < expectSalaryLow) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
function getTempTargetJobIndexToCheckDetail () {
|
||||
return jobListData.findIndex(it => {
|
||||
return !blockBossNotNewChat.has(it.encryptBossId) &&
|
||||
@@ -571,7 +812,7 @@ async function toRecommendPage (hooks) {
|
||||
:
|
||||
true
|
||||
) || (
|
||||
// enter job detail to mark as not suit
|
||||
// enter job detail to mark as not suit for city filter
|
||||
(
|
||||
Array.isArray(expectCityList) &&
|
||||
expectCityList.length &&
|
||||
@@ -581,6 +822,27 @@ async function toRecommendPage (hooks) {
|
||||
].includes(expectCityNotMatchStrategy) &&
|
||||
strategyScopeOptionWhenMarkJobCityNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
|
||||
) ? !expectCityList.includes(it.cityName) : false
|
||||
) || (
|
||||
// enter job detail to mark as not suit for work exp filter
|
||||
(
|
||||
Array.isArray(expectWorkExpList) &&
|
||||
expectWorkExpList.length &&
|
||||
[
|
||||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
].includes(expectWorkExpNotMatchStrategy) &&
|
||||
strategyScopeOptionWhenMarkJobWorkExpNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
|
||||
) ? !expectWorkExpList.includes(it.jobExperience) : false
|
||||
) || (
|
||||
// enter job detail to mark as not suit for salary filter
|
||||
(
|
||||
isSalaryFilterEnabled &&
|
||||
[
|
||||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
].includes(expectSalaryNotMatchStrategy) &&
|
||||
strategyScopeOptionWhenMarkSalaryNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
|
||||
) ? !checkIfSalarySuit(it.salaryDesc) : false
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -664,6 +926,196 @@ async function toRecommendPage (hooks) {
|
||||
// save the job detail info
|
||||
await hooks.jobDetailIsGetFromRecommendList?.promise(targetJobData)
|
||||
|
||||
//#region collect not suit reasons
|
||||
const notSuitReasonIdToStrategyMap = {}
|
||||
const notSuitConditionHandleMap = {
|
||||
async active() {
|
||||
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
|
||||
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.BOSS_INACTIVE)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||||
extInfo: {
|
||||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
},
|
||||
async city() {
|
||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_CITY_NOT_SUIT,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_CITY_NOT_SUIT,
|
||||
extInfo: {
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
},
|
||||
async workExp() {
|
||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||
if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT,
|
||||
extInfo: {
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
},
|
||||
async jobDetail() {
|
||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||||
extInfo: {
|
||||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
},
|
||||
async salary() {
|
||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||
if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT,
|
||||
extInfo: {
|
||||
salaryDesc: selectedJobData.salaryDesc,
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT,
|
||||
extInfo: {
|
||||
salaryDesc: selectedJobData.salaryDesc,
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#region
|
||||
// null
|
||||
// 刚刚活跃 // 今日活跃 // 昨日活跃 // 3日内活跃 // 本周活跃 // 2周内活跃
|
||||
@@ -674,122 +1126,56 @@ async function toRecommendPage (hooks) {
|
||||
markAsNotActiveSelectedTimeRange > 0 &&
|
||||
indexOfActiveText > 0 && indexOfActiveText <= markAsNotActiveSelectedTimeRange
|
||||
) {
|
||||
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
|
||||
// click prevent recommend button
|
||||
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.BOSS_INACTIVE)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||||
extInfo: {
|
||||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
continue continueFind
|
||||
notSuitReasonIdToStrategyMap.active = jobNotActiveStrategy
|
||||
}
|
||||
if (
|
||||
(Array.isArray(expectCityList) && expectCityList.length) && !expectCityList.includes(selectedJobData.cityName)
|
||||
) {
|
||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_CITY_NOT_SUIT,
|
||||
extInfo: {
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_CITY_NOT_SUIT,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
continue continueFind
|
||||
notSuitReasonIdToStrategyMap.city = expectCityNotMatchStrategy
|
||||
}
|
||||
if (
|
||||
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo)
|
||||
(Array.isArray(expectWorkExpList) && expectWorkExpList.length) && !expectWorkExpList.includes(selectedJobData.jobExperience)
|
||||
) {
|
||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||||
extInfo: {
|
||||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
notSuitReasonIdToStrategyMap.workExp = expectWorkExpNotMatchStrategy
|
||||
}
|
||||
if (
|
||||
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo, jobDetailRegExpMatchLogic)
|
||||
) {
|
||||
notSuitReasonIdToStrategyMap.jobDetail = jobNotMatchStrategy
|
||||
}
|
||||
if (
|
||||
!checkIfSalarySuit(selectedJobData.salaryDesc)
|
||||
) {
|
||||
notSuitReasonIdToStrategyMap.salary = expectSalaryNotMatchStrategy
|
||||
}
|
||||
// #endregion
|
||||
console.log('not suit reason and related strategy: ', notSuitReasonIdToStrategyMap)
|
||||
|
||||
// #region execute mark logic
|
||||
// 1. find the one mark on Boss
|
||||
const markOnBossCondition = Object.keys(notSuitReasonIdToStrategyMap).find(k => notSuitReasonIdToStrategyMap[k] === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS)
|
||||
if (markOnBossCondition) {
|
||||
await notSuitConditionHandleMap[markOnBossCondition]()
|
||||
continue continueFind
|
||||
}
|
||||
// test company again - when allow list not include target company, just skip
|
||||
// 2. if there is no condition to mark Boss, then find the one mark on local db
|
||||
const markOnLocalDbCondition = Object.keys(notSuitReasonIdToStrategyMap).find(k => notSuitReasonIdToStrategyMap[k] === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL)
|
||||
if (markOnLocalDbCondition) {
|
||||
await notSuitConditionHandleMap[markOnLocalDbCondition]()
|
||||
continue continueFind
|
||||
}
|
||||
// #endregion
|
||||
if (
|
||||
// test company again - when allow list not include target company, just skip
|
||||
enableCompanyAllowList && ![...expectCompanySet].find(
|
||||
name => selectedJobData.brandName?.toLowerCase?.()?.includes(name.toLowerCase())
|
||||
)
|
||||
) ||
|
||||
// check if job has been marked as not suit or not active
|
||||
[
|
||||
...blockJobNotSuit,
|
||||
...blockBossNotActive
|
||||
].includes(targetJobData.jobInfo.encryptId)
|
||||
) {
|
||||
// just skip
|
||||
continue continueFind
|
||||
@@ -851,7 +1237,13 @@ async function toRecommendPage (hooks) {
|
||||
throw new Error('STARTUP_CHAT_ERROR_WITH_UNKNOWN_ERROR')
|
||||
}
|
||||
} else {
|
||||
await hooks.newChatStartup?.promise(targetJobData, { chatStartupFrom: ChatStartupFrom.AutoFromRecommendList })
|
||||
await hooks.newChatStartup?.promise(
|
||||
targetJobData,
|
||||
{
|
||||
chatStartupFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
|
||||
}
|
||||
)
|
||||
blockBossNotNewChat.add(targetJobData.jobInfo.encryptUserId)
|
||||
|
||||
await storeStorage(page).catch(() => void 0)
|
||||
@@ -894,20 +1286,20 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
// for of reach terminal
|
||||
if (
|
||||
currentExceptJobIndex + 1 >= expectJobList.length
|
||||
currentSourceIndex + 1 >= computedSourceList.length
|
||||
) {
|
||||
hooks.noPositionFoundForCurrentJob?.call()
|
||||
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
||||
await sleep((20 + 30 * Math.random()) * 1000)
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.goto(`https://www.zhipin.com/web/geek/jobs`),
|
||||
page.waitForNavigation()
|
||||
])
|
||||
currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||||
currentSourceIndex = 0
|
||||
} else {
|
||||
hooks.noPositionFoundForCurrentJob?.call()
|
||||
await sleep((10 + 15 * Math.random()) * 1000)
|
||||
currentExceptJobIndex += 1
|
||||
currentSourceIndex += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -934,12 +1326,13 @@ export async function mainLoop (hooks) {
|
||||
}
|
||||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
|
||||
await page.bringToFront()
|
||||
await hooks.mainFlowWillLaunch?.callAsync({
|
||||
await hooks.mainFlowWillLaunch?.promise({
|
||||
jobNotMatchStrategy,
|
||||
jobNotActiveStrategy,
|
||||
expectCityNotMatchStrategy,
|
||||
blockJobNotSuit,
|
||||
blockBossNotActive,
|
||||
blockBossNotNewChat
|
||||
})
|
||||
await toRecommendPage(hooks)
|
||||
// goto search
|
||||
|
||||
@@ -56,7 +56,8 @@ const main = async () => {
|
||||
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
|
||||
noPositionFoundForCurrentJob: new SyncHook(),
|
||||
noPositionFoundAfterTraverseAllJob: new SyncHook(),
|
||||
errorEncounter: new SyncHook(['errorInfo'])
|
||||
errorEncounter: new SyncHook(['errorInfo']),
|
||||
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args'])
|
||||
}
|
||||
initPlugins(hooks)
|
||||
await hooks.daemonInitialized.callAsync()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { JobSource } from "../enums";
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
||||
|
||||
@@ -29,4 +30,9 @@ export class ChatStartupLog {
|
||||
nullable: true
|
||||
})
|
||||
autoStartupChatRecordId?: number;
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
jobSource?: JobSource;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
|
||||
import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
import { ChatStartupFrom } from "./ChatStartupLog";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
||||
@@ -41,4 +41,9 @@ export class MarkAsNotSuitLog {
|
||||
nullable: true
|
||||
})
|
||||
autoStartupChatRecordId?: number;
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
jobSource?: JobSource;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ export enum MarkAsNotSuitReason {
|
||||
USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON = 2,
|
||||
JOB_NOT_SUIT = 3,
|
||||
JOB_CITY_NOT_SUIT = 4,
|
||||
JOB_WORK_EXP_NOT_SUIT = 5,
|
||||
JOB_SALARY_NOT_SUIT = 6,
|
||||
}
|
||||
|
||||
export enum MarkAsNotSuitOp {
|
||||
@@ -16,3 +18,19 @@ export enum StrategyScopeOptionWhenMarkJobNotMatch {
|
||||
ALL_JOB = 1,
|
||||
ONLY_COMPANY_MATCHED_JOB = 2
|
||||
}
|
||||
|
||||
export enum SalaryCalculateWay {
|
||||
MONTH_SALARY = 1,
|
||||
ANNUAL_PACKAGE = 2,
|
||||
}
|
||||
|
||||
export enum JobDetailRegExpMatchLogic {
|
||||
EVERY = 1,
|
||||
SOME = 2,
|
||||
}
|
||||
|
||||
export enum JobSource {
|
||||
expect = 1,
|
||||
recommend = 2,
|
||||
search = 3,
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ export async function saveChatStartupRecord(
|
||||
ds: DataSource,
|
||||
_jobInfo,
|
||||
{ encryptUserId },
|
||||
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined } = {}
|
||||
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined, jobSource = undefined } = {}
|
||||
) {
|
||||
const { jobInfo } = _jobInfo;
|
||||
|
||||
@@ -256,7 +256,8 @@ export async function saveChatStartupRecord(
|
||||
encryptCurrentUserId: encryptUserId,
|
||||
encryptJobId: jobInfo.encryptId,
|
||||
autoStartupChatRecordId,
|
||||
chatStartupFrom
|
||||
chatStartupFrom,
|
||||
jobSource,
|
||||
}
|
||||
Object.assign(chatStartupLog, chatStartupLogPayload)
|
||||
|
||||
@@ -270,7 +271,7 @@ export async function saveMarkAsNotSuitRecord(
|
||||
ds: DataSource,
|
||||
_jobInfo,
|
||||
{ encryptUserId },
|
||||
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined } = {}
|
||||
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined, jobSource = undefined } = {}
|
||||
) {
|
||||
const { jobInfo } = _jobInfo;
|
||||
|
||||
@@ -284,7 +285,8 @@ export async function saveMarkAsNotSuitRecord(
|
||||
markFrom,
|
||||
markReason,
|
||||
extInfo: extInfo ? JSON.stringify(extInfo) : undefined,
|
||||
markOp
|
||||
markOp,
|
||||
jobSource,
|
||||
}
|
||||
Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload)
|
||||
|
||||
@@ -333,9 +335,33 @@ export async function getNotSuitMarkRecordsInLastSomeDays (ds: DataSource, days
|
||||
const result = await repo.findBy({
|
||||
date: Raw(alias => `DATE(${alias}) >= DATE('${
|
||||
new Date(
|
||||
Number(new Date()) - 7 * 24 * 60 * 60 * 1000
|
||||
Number(new Date()) - days * 24 * 60 * 60 * 1000
|
||||
).toISOString()
|
||||
}')`)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getChatStartupRecordsInLastSomeDays (ds: DataSource, days = 0) {
|
||||
const repo = ds.getRepository(ChatStartupLog)
|
||||
const result = await repo.findBy({
|
||||
date: Raw(alias => `DATE(${alias}) >= DATE('${
|
||||
new Date(
|
||||
Number(new Date()) - days * 24 * 60 * 60 * 1000
|
||||
).toISOString()
|
||||
}')`)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getBossIdsByJobIds (ds: DataSource, jobIds: string[] = []) {
|
||||
const repo = ds.getRepository(JobInfo)
|
||||
const result = await repo.find({
|
||||
where: jobIds.map(
|
||||
id => ({
|
||||
encryptJobId: id
|
||||
})
|
||||
)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -23,13 +23,21 @@ import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
||||
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
||||
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord, getNotSuitMarkRecordsInLastSomeDays } from "./handlers";
|
||||
import {
|
||||
saveChatStartupRecord,
|
||||
saveJobInfoFromRecommendPage,
|
||||
saveMarkAsNotSuitRecord,
|
||||
getNotSuitMarkRecordsInLastSomeDays,
|
||||
getChatStartupRecordsInLastSomeDays,
|
||||
getBossIdsByJobIds
|
||||
} from "./handlers";
|
||||
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
|
||||
import minimist from 'minimist'
|
||||
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
|
||||
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
|
||||
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
|
||||
import { Init1000000000000 } from "./migrations/1000000000000-Init";
|
||||
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
|
||||
|
||||
export function initDb(dbFilePath) {
|
||||
const { DataSource } = requireTypeorm()
|
||||
@@ -65,6 +73,7 @@ export function initDb(dbFilePath) {
|
||||
UpdateChatStartupLogTable1729182577167,
|
||||
UpdateBossInfoTable1732032381304,
|
||||
AddColumnForMarkAsNotSuitLog1746092370665,
|
||||
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526
|
||||
],
|
||||
migrationsRun: true
|
||||
});
|
||||
@@ -111,6 +120,7 @@ export default class SqlitePlugin {
|
||||
expectCityNotMatchStrategy,
|
||||
blockJobNotSuit,
|
||||
blockBossNotActive,
|
||||
blockBossNotNewChat
|
||||
}) => {
|
||||
if (
|
||||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
@@ -118,8 +128,11 @@ export default class SqlitePlugin {
|
||||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
) {
|
||||
const ds = await this.initPromise;
|
||||
const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7) ?? []);
|
||||
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||||
const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7)) ?? [];
|
||||
if (
|
||||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
) {
|
||||
last7DayMarkRecords
|
||||
.filter(it =>
|
||||
[
|
||||
@@ -134,7 +147,10 @@ export default class SqlitePlugin {
|
||||
id => blockJobNotSuit.add(id)
|
||||
)
|
||||
}
|
||||
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||||
if (
|
||||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
) {
|
||||
last7DayMarkRecords
|
||||
.filter(it => it.markReason === MarkAsNotSuitReason.BOSS_INACTIVE)
|
||||
.map(
|
||||
@@ -144,7 +160,10 @@ export default class SqlitePlugin {
|
||||
id => blockJobNotSuit.add(id)
|
||||
)
|
||||
}
|
||||
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||||
if (
|
||||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
) {
|
||||
last7DayMarkRecords
|
||||
.filter(it => it.markReason === MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
|
||||
.map(
|
||||
@@ -154,6 +173,12 @@ export default class SqlitePlugin {
|
||||
id => blockJobNotSuit.add(id)
|
||||
)
|
||||
}
|
||||
const last30DayChatStartupRecords = (await getChatStartupRecordsInLastSomeDays(ds, 30)) ?? [];
|
||||
const chattedJobIds = last30DayChatStartupRecords.map(it => it.encryptJobId)
|
||||
const chattedBossIds = ((await getBossIdsByJobIds(ds, chattedJobIds)) ?? []).map(it => it.encryptBossId)
|
||||
for (const id of chattedBossIds) {
|
||||
blockBossNotNewChat.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -163,22 +188,24 @@ export default class SqlitePlugin {
|
||||
await saveJobInfoFromRecommendPage(ds, _jobInfo);
|
||||
});
|
||||
|
||||
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList } = {}) => {
|
||||
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList, jobSource = undefined } = {}) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
|
||||
autoStartupChatRecordId: this.runRecordId,
|
||||
chatStartupFrom
|
||||
chatStartupFrom,
|
||||
jobSource
|
||||
});
|
||||
});
|
||||
|
||||
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined } = {}) => {
|
||||
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined, jobSource = undefined } = {}) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, {
|
||||
autoStartupChatRecordId: this.runRecordId,
|
||||
markFrom,
|
||||
markReason,
|
||||
extInfo,
|
||||
markOp
|
||||
markOp,
|
||||
jobSource
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm"
|
||||
import { VBossLibrary } from "../entity/VBossLibrary";
|
||||
import { VChatStartupLog } from "../entity/VChatStartupLog";
|
||||
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
|
||||
import { VJobLibrary } from "../entity/VJobLibrary";
|
||||
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
|
||||
import { JobSource } from "../enums";
|
||||
|
||||
const ViewEntities = [
|
||||
VBossLibrary,
|
||||
VChatStartupLog,
|
||||
VCompanyLibrary,
|
||||
VJobLibrary,
|
||||
VMarkAsNotSuitLog,
|
||||
]
|
||||
export class AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
|
||||
}
|
||||
if (await queryRunner.hasTable("mark_as_not_suit_log")) {
|
||||
if (!await queryRunner.hasColumn("mark_as_not_suit_log", "jobSource")) {
|
||||
await queryRunner.addColumn(
|
||||
"mark_as_not_suit_log",
|
||||
new TableColumn({
|
||||
name: "jobSource",
|
||||
type: "number",
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
await queryRunner.query(`UPDATE mark_as_not_suit_log SET jobSource=?`, [JobSource.expect]);
|
||||
}
|
||||
}
|
||||
if (await queryRunner.hasTable("chat_startup_log")) {
|
||||
if (!await queryRunner.hasColumn("chat_startup_log", "jobSource")) {
|
||||
await queryRunner.addColumn(
|
||||
"chat_startup_log",
|
||||
new TableColumn({
|
||||
name: "jobSource",
|
||||
type: "number",
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
await queryRunner.query(`UPDATE chat_startup_log SET jobSource=?`, [JobSource.expect]);
|
||||
}
|
||||
}
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
let expression = viewMetadata.expression;
|
||||
if (typeof expression === 'function') {
|
||||
expression = expression(dataSource).getQuery();
|
||||
}
|
||||
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "geekgeekrun-ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.9.0",
|
||||
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "geekgeekrun",
|
||||
@@ -44,7 +44,8 @@
|
||||
"pinia": "^3.0.2",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^11.1.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
@@ -66,7 +67,7 @@
|
||||
"electron": "28.2.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"element-plus": "^2.5.5",
|
||||
"element-plus": "^2.8.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"find-chrome-bin": "^2.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "0.7.1",
|
||||
"buildVersion": 14,
|
||||
"buildTime": 1748459396794,
|
||||
"buildHash": "d479687b8ba13c6a7bb81dbade1c29317add46e7",
|
||||
"version": "0.9.0",
|
||||
"buildVersion": 16,
|
||||
"buildTime": 1752412320912,
|
||||
"buildHash": "d3e086f32184dd2a36819c0f9697228fa1316ada",
|
||||
"name": "geekgeekrun-ui"
|
||||
}
|
||||
@@ -93,7 +93,8 @@ const runAutoChat = async () => {
|
||||
jobMarkedAsNotSuit: new AsyncSeriesHook(['positionInfoDetail', 'markDetail']),
|
||||
noPositionFoundForCurrentJob: new SyncHook(),
|
||||
noPositionFoundAfterTraverseAllJob: new SyncHook(),
|
||||
errorEncounter: new SyncHook(['errorInfo'])
|
||||
errorEncounter: new SyncHook(['errorInfo']),
|
||||
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args'])
|
||||
}
|
||||
initPlugins(hooks)
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import {
|
||||
RECOMMEND_JOB_ENTRY_SELECTOR,
|
||||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs'
|
||||
import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
|
||||
import {
|
||||
saveJobInfoFromRecommendPage,
|
||||
@@ -13,7 +17,7 @@ import {
|
||||
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||
import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import { MarkAsNotSuitReason, JobSource } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import { Target } from 'puppeteer'
|
||||
@@ -38,6 +42,49 @@ const attachRequestsListener = async (target: Target) => {
|
||||
if (!page) {
|
||||
return
|
||||
}
|
||||
async function getCurrentJobSource() {
|
||||
const methodMap = {
|
||||
async recommend() {
|
||||
return await page.evaluate(
|
||||
({ RECOMMEND_JOB_ENTRY_SELECTOR }) => {
|
||||
return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active')
|
||||
},
|
||||
{
|
||||
RECOMMEND_JOB_ENTRY_SELECTOR
|
||||
}
|
||||
)
|
||||
},
|
||||
async expect() {
|
||||
return await page.evaluate(
|
||||
({ USER_SET_EXPECT_JOB_ENTRIES_SELECTOR }) => {
|
||||
return [...document.querySelectorAll(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)].some((el) =>
|
||||
el.classList.contains('active')
|
||||
)
|
||||
},
|
||||
{
|
||||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR
|
||||
}
|
||||
)
|
||||
},
|
||||
async search() {
|
||||
const elHandle = await page.$(`.page-jobs-main`)
|
||||
const currentKeyWord = await elHandle?.evaluate((el) => {
|
||||
return el?.__vue__?.formData?.query
|
||||
})
|
||||
return !!currentKeyWord
|
||||
}
|
||||
}
|
||||
for (const [type, func] of Object.entries(methodMap)) {
|
||||
try {
|
||||
if (await func()) {
|
||||
return type
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('encounter error when get job source')
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
page.on('response', async (response) => {
|
||||
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
|
||||
@@ -81,7 +128,7 @@ const attachRequestsListener = async (target: Target) => {
|
||||
const reasonCodeToTextMap = await readStorageFile(
|
||||
'job-not-suit-reason-code-to-text-cache.json'
|
||||
)
|
||||
|
||||
const jobSource = await getCurrentJobSource()
|
||||
const markDetail = {
|
||||
markFrom: ChatStartupFrom.ManuallyFromRecommendList,
|
||||
extInfo: {
|
||||
@@ -90,12 +137,14 @@ const attachRequestsListener = async (target: Target) => {
|
||||
text: reasonCodeToTextMap[chosenCode]
|
||||
}
|
||||
},
|
||||
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON
|
||||
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON,
|
||||
jobSource: JobSource[jobSource]
|
||||
}
|
||||
gtag('job_marked_as_not_suit', {
|
||||
markFrom: markDetail.markFrom,
|
||||
bossActiveTimeDesc: currentJobData?.bossInfo?.activeTimeDesc,
|
||||
encryptJobId: currentJobData?.jobInfo?.encryptId
|
||||
encryptJobId: currentJobData?.jobInfo?.encryptId,
|
||||
jobSource: JobSource[jobSource]
|
||||
})
|
||||
if (reasonCodeToTextMap[chosenCode]?.includes('活跃度低')) {
|
||||
markDetail.markReason = MarkAsNotSuitReason.BOSS_INACTIVE
|
||||
@@ -131,9 +180,11 @@ const attachRequestsListener = async (target: Target) => {
|
||||
const currentUserInfo = await page.evaluate(
|
||||
'document.querySelector(".job-detail-box").__vue__.$store.state.userInfo'
|
||||
)
|
||||
const jobSource = await getCurrentJobSource()
|
||||
gtag('new_chat_startup', {
|
||||
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
|
||||
encryptJobId: currentJobData?.jobInfo?.encryptId
|
||||
encryptJobId: currentJobData?.jobInfo?.encryptId,
|
||||
jobSource: JobSource[jobSource]
|
||||
})
|
||||
await saveChatStartupRecord(
|
||||
await dbInitPromise,
|
||||
@@ -142,7 +193,8 @@ const attachRequestsListener = async (target: Target) => {
|
||||
encryptUserId: currentUserInfo.encryptUserId
|
||||
},
|
||||
{
|
||||
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList
|
||||
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
|
||||
jobSource: JobSource[jobSource]
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
|
||||
@@ -107,6 +107,8 @@ export default function initIpc() {
|
||||
if (hasOwn(payload, 'autoReminder')) {
|
||||
bossConfig.autoReminder = payload.autoReminder
|
||||
}
|
||||
|
||||
// city
|
||||
if (hasOwn(payload, 'expectCityList')) {
|
||||
bossConfig.expectCityList = payload.expectCityList
|
||||
}
|
||||
@@ -117,6 +119,47 @@ export default function initIpc() {
|
||||
bossConfig.strategyScopeOptionWhenMarkJobCityNotMatch =
|
||||
payload.strategyScopeOptionWhenMarkJobCityNotMatch
|
||||
}
|
||||
|
||||
// salary
|
||||
if (hasOwn(payload, 'expectSalaryCalculateWay')) {
|
||||
bossConfig.expectSalaryCalculateWay = payload.expectSalaryCalculateWay
|
||||
}
|
||||
if (hasOwn(payload, 'expectSalaryNotMatchStrategy')) {
|
||||
bossConfig.expectSalaryNotMatchStrategy = payload.expectSalaryNotMatchStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'strategyScopeOptionWhenMarkSalaryNotMatch')) {
|
||||
bossConfig.strategyScopeOptionWhenMarkSalaryNotMatch =
|
||||
payload.strategyScopeOptionWhenMarkSalaryNotMatch
|
||||
}
|
||||
if (hasOwn(payload, 'expectSalaryLow')) {
|
||||
bossConfig.expectSalaryLow = payload.expectSalaryLow
|
||||
}
|
||||
if (hasOwn(payload, 'expectSalaryHigh')) {
|
||||
bossConfig.expectSalaryHigh = payload.expectSalaryHigh
|
||||
}
|
||||
|
||||
// work exp
|
||||
if (hasOwn(payload, 'expectWorkExpList')) {
|
||||
bossConfig.expectWorkExpList = payload.expectWorkExpList
|
||||
}
|
||||
if (hasOwn(payload, 'expectWorkExpNotMatchStrategy')) {
|
||||
bossConfig.expectWorkExpNotMatchStrategy = payload.expectWorkExpNotMatchStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'strategyScopeOptionWhenMarkJobWorkExpNotMatch')) {
|
||||
bossConfig.strategyScopeOptionWhenMarkJobWorkExpNotMatch =
|
||||
payload.strategyScopeOptionWhenMarkJobWorkExpNotMatch
|
||||
}
|
||||
if (hasOwn(payload, 'jobDetailRegExpMatchLogic')) {
|
||||
bossConfig.jobDetailRegExpMatchLogic = payload.jobDetailRegExpMatchLogic
|
||||
}
|
||||
if (hasOwn(payload, 'isSkipEmptyConditionForCombineRecommendJobFilter')) {
|
||||
bossConfig.isSkipEmptyConditionForCombineRecommendJobFilter =
|
||||
payload.isSkipEmptyConditionForCombineRecommendJobFilter
|
||||
}
|
||||
if (hasOwn(payload, 'jobSourceList')) {
|
||||
bossConfig.jobSourceList = payload.jobSourceList
|
||||
}
|
||||
|
||||
promiseArr.push(writeConfigFile('boss.json', bossConfig))
|
||||
|
||||
if (hasOwn(payload, 'expectCompanies')) {
|
||||
|
||||
@@ -2,26 +2,34 @@ import gtag from '.'
|
||||
|
||||
export default class GtagPlugin {
|
||||
apply(hooks) {
|
||||
hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom }) => {
|
||||
hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom, jobSource }) => {
|
||||
gtag('new_chat_startup', {
|
||||
chatStartupFrom,
|
||||
encryptJobId: jobData.jobInfo.encryptId
|
||||
})
|
||||
})
|
||||
hooks.jobMarkedAsNotSuit.tap('GtagPlugin', (jobData, { markFrom, markOp, markReason }) => {
|
||||
gtag('job_marked_as_not_suit', {
|
||||
markFrom,
|
||||
markOp,
|
||||
markReason,
|
||||
bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc,
|
||||
encryptJobId: jobData.jobInfo.encryptId
|
||||
encryptJobId: jobData.jobInfo.encryptId,
|
||||
jobSource
|
||||
})
|
||||
})
|
||||
hooks.jobMarkedAsNotSuit.tap(
|
||||
'GtagPlugin',
|
||||
(jobData, { markFrom, markOp, markReason, jobSource }) => {
|
||||
gtag('job_marked_as_not_suit', {
|
||||
markFrom,
|
||||
markOp,
|
||||
markReason,
|
||||
bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc,
|
||||
encryptJobId: jobData.jobInfo.encryptId,
|
||||
jobSource
|
||||
})
|
||||
}
|
||||
)
|
||||
hooks.noPositionFoundForCurrentJob.tap('GtagPlugin', () => {
|
||||
gtag('no_position_found_for_current_job')
|
||||
})
|
||||
hooks.noPositionFoundAfterTraverseAllJob.tap('GtagPlugin', () => {
|
||||
gtag('no_position_found_after_traverse_all_job')
|
||||
})
|
||||
hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => {
|
||||
gtag('encounter_empty_rec_job_list', { pageQuery })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function createMainWindow(): BrowserWindow {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 720,
|
||||
minWidth: 1024,
|
||||
minWidth: 1280,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux'
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<draggable
|
||||
:model-value="modelValue"
|
||||
class="list-group"
|
||||
:component-data="{
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !drag ? 'flip-list' : null
|
||||
}"
|
||||
:group="{ name: 'parent', put: ['parent'] }"
|
||||
v-bind="dragOptions"
|
||||
handle=".drag-handle-wrap"
|
||||
@update:model-value="emit('update:model-value', $event)"
|
||||
@start="drag = true"
|
||||
@end="
|
||||
() => {
|
||||
drag = false
|
||||
setTimeout(() => {
|
||||
gtagRenderer('job-source-dragged', {
|
||||
sourceOrder: modelValue?.map((it) => JobSource[it.type]).join(','),
|
||||
enabledSourceOrder: modelValue
|
||||
?.filter?.((it) => !!it.enabled)
|
||||
?.map((it) => JobSource[it.type])
|
||||
?.join(',')
|
||||
})
|
||||
}, 50)
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<li class="list-group-item">
|
||||
<div flex flex-items-center>
|
||||
<span class="drag-handle-wrap">
|
||||
<span class="drag-handle" />
|
||||
</span>
|
||||
<span class="inner-content">
|
||||
<div flex w-full flex-1>
|
||||
<template
|
||||
v-if="
|
||||
element.type === 'search' &&
|
||||
(!element?.children?.length ||
|
||||
element?.children?.every((it) => !(it.enabled && !!it.keyword?.trim())))
|
||||
"
|
||||
>
|
||||
<el-switch
|
||||
model-value="false"
|
||||
mr10px
|
||||
disabled
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@click="
|
||||
() => {
|
||||
if (!element?.children?.length) {
|
||||
Message.info({
|
||||
message: '请添加一个要搜索的关键词',
|
||||
grouping: true
|
||||
})
|
||||
} else if (
|
||||
element?.children?.every((it) => !(it.enabled && !!it.keyword?.trim()))
|
||||
) {
|
||||
Message.info({
|
||||
message: '请启用一个要搜索的关键词',
|
||||
grouping: true
|
||||
})
|
||||
}
|
||||
gtagRenderer('job-source-switch-ph-clicked', { type: element.type })
|
||||
}
|
||||
"
|
||||
/>
|
||||
{{ element.label }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-switch
|
||||
v-model="element.enabled"
|
||||
mr10px
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@change="
|
||||
(v) => gtagRenderer('job-source-switch-changed', { type: element.type, v })
|
||||
"
|
||||
/>
|
||||
{{ element.label }}
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="element.type === 'search'">
|
||||
<div flex flex-items-center>
|
||||
<span color-orange align-self-end mr10px>
|
||||
{{ getSearchSourceTipText(element) }}
|
||||
</span>
|
||||
<el-button
|
||||
p-0
|
||||
h-fit
|
||||
type="text"
|
||||
@click="
|
||||
() => {
|
||||
addSearchKeyword(element)
|
||||
gtagRenderer('job-source-search-kw-added', {
|
||||
kwListLength: element.children?.length
|
||||
})
|
||||
}
|
||||
"
|
||||
>添加关键词</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="element.type === 'search' && element?.children?.length">
|
||||
<draggable
|
||||
v-model="element.children"
|
||||
class="list-group"
|
||||
:component-data="{
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !drag ? 'flip-list' : null
|
||||
}"
|
||||
:group="{ name: 'child', put: ['child'] }"
|
||||
v-bind="dragOptions"
|
||||
handle=".drag-handle-wrap"
|
||||
@start="drag = true"
|
||||
@end="
|
||||
() => {
|
||||
drag = false
|
||||
setTimeout(() => {
|
||||
gtagRenderer('job-source-search-kw-dragged', {
|
||||
kwListLength: element.children?.length
|
||||
})
|
||||
}, 50)
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #item="{ element: searchItem, index }">
|
||||
<li class="list-group-item">
|
||||
<div flex flex-items-center>
|
||||
<span class="drag-handle-wrap">
|
||||
<span class="drag-handle" />
|
||||
</span>
|
||||
<span class="inner-content">
|
||||
<div flex w-full>
|
||||
<el-switch
|
||||
v-if="element.enabled && searchItem.keyword?.trim()"
|
||||
v-model="searchItem.enabled"
|
||||
mr10px
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@change="(v) => gtagRenderer('job-source-search-kw-switch-changed', { v })"
|
||||
/>
|
||||
<el-switch
|
||||
v-else
|
||||
disabled
|
||||
:model-value="false"
|
||||
mr10px
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@click="
|
||||
() => {
|
||||
if (!searchItem.keyword?.trim()) {
|
||||
Message.info({
|
||||
message: '该条关键词为空,请输入要搜索的关键词',
|
||||
grouping: true
|
||||
})
|
||||
} else if (!element.enabled) {
|
||||
Message.info({
|
||||
message: '请启用“通过搜索找到的职位”来源',
|
||||
grouping: true
|
||||
})
|
||||
}
|
||||
gtagRenderer('job-source-search-kw-switch-ph-clicked')
|
||||
}
|
||||
"
|
||||
/>
|
||||
<el-input
|
||||
v-model="searchItem.keyword"
|
||||
maxlength="100"
|
||||
@blur="
|
||||
() => {
|
||||
searchItem.keyword = searchItem.keyword?.trim() ?? ''
|
||||
gtagRenderer('job-source-search-kw-input-blurred', {
|
||||
contentLength: searchItem.keyword?.length
|
||||
})
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
p-0
|
||||
ml-10px
|
||||
h-fit
|
||||
type="danger"
|
||||
link
|
||||
@click="
|
||||
() => {
|
||||
removeSearchKeywordByIndex(element, index)
|
||||
gtagRenderer('job-source-search-kw-removed', {
|
||||
itemIndex: index,
|
||||
contentLength: searchItem.keyword?.length
|
||||
})
|
||||
}
|
||||
"
|
||||
>删除</el-button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { ElMessage as Message } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { JobSource } from '@geekgeekrun/sqlite-plugin/src/enums'
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array
|
||||
}
|
||||
})
|
||||
|
||||
const setTimeout = globalThis.setTimeout.bind(globalThis)
|
||||
|
||||
const emit = defineEmits(['update:model-value'])
|
||||
|
||||
const drag = ref(false)
|
||||
|
||||
const dragOptions = computed(() => {
|
||||
return {
|
||||
animation: 200,
|
||||
disabled: false,
|
||||
ghostClass: 'ghost'
|
||||
}
|
||||
})
|
||||
|
||||
function addSearchKeyword(item) {
|
||||
if (!item.children) {
|
||||
item.children = []
|
||||
}
|
||||
item.enabled = true
|
||||
item.children.push({
|
||||
type: 'search-kw',
|
||||
enabled: true,
|
||||
keyword: ''
|
||||
})
|
||||
}
|
||||
|
||||
function removeSearchKeywordByIndex(item, index) {
|
||||
item.children?.splice(index, 1)
|
||||
}
|
||||
const searchKeywordCountMap = computed(() => {
|
||||
const target = props.modelValue?.find((it) => it.type === 'search' && it.enabled)
|
||||
if (!target || !target?.children?.length) {
|
||||
return {}
|
||||
}
|
||||
const map = {}
|
||||
target.children?.forEach((it) => {
|
||||
if (!it.keyword) {
|
||||
return
|
||||
}
|
||||
if (!map[it.keyword]) {
|
||||
map[it.keyword] = 0
|
||||
}
|
||||
map[it.keyword]++
|
||||
})
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
const getSearchSourceTipText = (element) => {
|
||||
if (!element?.children?.length) {
|
||||
return `添加一个关键词后方可启用->`
|
||||
}
|
||||
const seg: string[] = []
|
||||
if (element.enabled) {
|
||||
if (element.children?.every((it) => !(it.enabled && !!it.keyword?.trim()))) {
|
||||
seg.push(`启用下方任一非空项后方可启用`)
|
||||
} else if (element.children?.some((it) => it.enabled && !it.keyword?.trim())) {
|
||||
seg.push(`空项会被跳过`)
|
||||
}
|
||||
if (Object.values(searchKeywordCountMap.value).some((n) => n > 1)) {
|
||||
seg.push(`重复项仅取第一个启用的项`)
|
||||
}
|
||||
}
|
||||
return seg.join(';')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.list-group {
|
||||
line-height: 32px;
|
||||
min-height: 20px;
|
||||
.list-group {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.list-group-item {
|
||||
background-color: #fff;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-right: 10px;
|
||||
border: 1px solid var(--el-card-border-color);
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
border-radius: 4px;
|
||||
.inner-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
::v-deep(.el-checkbox) {
|
||||
.el-checkbox__label {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.drag-handle-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 25px;
|
||||
height: 1em;
|
||||
cursor: grab;
|
||||
margin-right: 4px;
|
||||
.drag-handle {
|
||||
width: 20px;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 35%,
|
||||
var(--el-card-border-color) 35%,
|
||||
var(--el-card-border-color) 45%,
|
||||
transparent 45%,
|
||||
transparent 55%,
|
||||
var(--el-card-border-color) 55%,
|
||||
var(--el-card-border-color) 65%,
|
||||
transparent 65%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -109,8 +109,14 @@ import { ElForm, ElMessage } from 'element-plus'
|
||||
import { ref, onUnmounted, onMounted } from 'vue'
|
||||
import { checkCookieListFormat } from '../../../../common/utils/cookie'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const gtagRenderer = (name, params?: object) => {
|
||||
return baseGtagRenderer(name, {
|
||||
scene: 'cookie-assistant',
|
||||
...params
|
||||
})
|
||||
}
|
||||
const router = useRouter()
|
||||
const cookieInvalid = ref(false)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="first-run-readme" ref="componentRootEl">
|
||||
<div ref="componentRootEl" class="first-run-readme">
|
||||
<div class="first-run-readme__inner">
|
||||
<div class="readme-title">欢迎使用GeekGeekRun!祝您求职顺利~</div>
|
||||
<div class="readme-desc">
|
||||
@@ -12,25 +12,32 @@
|
||||
>
|
||||
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
|
||||
本程序从某种程度上说属于辅助工具,与<el-link
|
||||
@click="
|
||||
electron.ipcRenderer.send(
|
||||
'open-external-link',
|
||||
'https://about.zhipin.com/agreement/'
|
||||
)
|
||||
color-blue
|
||||
@click.stop.prevent="
|
||||
() => {
|
||||
electron.ipcRenderer.send(
|
||||
'open-external-link',
|
||||
'https://about.zhipin.com/agreement/'
|
||||
)
|
||||
gtagRenderer('view_boss_agreement_clicked')
|
||||
}
|
||||
"
|
||||
>《Boss直聘用户协议》</el-link
|
||||
>相关条款相违背;
|
||||
根据该条款,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于<b
|
||||
class="color-red"
|
||||
>账号被强制退出登录</b
|
||||
>、<b class="color-red">账号被限制使用</b>、<b class="color-red">账号被封禁</b
|
||||
>(2023年3月版)相关条款相违背,您在注册Boss直聘时已签署过这一条款;根据该条款
|
||||
<i>七、用户的平台使用义务</i>、<i>八、违约责任</i>
|
||||
章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于<b class="color-red"
|
||||
>账号被强制退出登录、账号被限制使用、账号被封禁</b
|
||||
>等对您不利的风险;因此使用本程序即意味着<b class="color-red">您愿意接受以上风险</b
|
||||
>,且如果相关风险发生,<b class="color-red">您需要自行承担相关后果</b>,<b
|
||||
class="color-red"
|
||||
>本程序不负责</b
|
||||
>。
|
||||
>,且如果相关风险发生,<b class="color-red">您需要自行承担相关后果,本程序不负责</b>。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
|
||||
本程序需要存储您的登录凭据,即Cookie,来模拟您在Boss直聘上开聊Boss的行为;本程序仅会把您的Cookie存储在本地,并在您访问Boss直聘时将其传输到Boss直聘,<b
|
||||
class="color-red"
|
||||
>不会泄露给第三方</b
|
||||
>,也不会进行除自动开聊Boss以外的行为;<b class="color-red">请勿向他人泄漏您的Cookie</b
|
||||
>。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
|
||||
本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时<b
|
||||
class="color-red"
|
||||
>注意节制</b
|
||||
@@ -38,39 +45,58 @@
|
||||
>注册一个本程序专用的新的Boss直聘账号</b
|
||||
>进行求职。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
|
||||
本程序原理是模拟用户在Boss直聘网页上进行点击操作;Boss直聘网站每过一段时间会发生改版,且有可能包含A/B实验,这将导致本程序相关脚本失效,此时您可以点击程序左下角进行反馈。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
|
||||
您的雇主可能会对您的计算机终端或网络进行监控,从而审计、跟踪您的行为;建议您<b
|
||||
本程序原理是模拟用户在Boss直聘网页上,寻找关键元素并进行点击操作;Boss直聘网站经常<b>发生改版</b>,且有可能<b>包含A/B实验</b>,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,<b
|
||||
class="color-red"
|
||||
>不要在您雇主提供的计算机终端或网络上使用本程序</b
|
||||
>。
|
||||
>浏览器重复“闪退、重新启动”</b
|
||||
>)。如果您在使用过程中遇上程序未按照预期执行的情况,请点击程序左下角进行反馈。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
|
||||
本程序需要存储您的登录凭据,即Cookie,来模拟您在Boss直聘上开聊Boss的行为;本程序仅会把您的Cookie存储在本地,并在您访问Boss直聘时将其传输到Boss直聘,<b
|
||||
class="color-red"
|
||||
>不会泄露给第三方</b
|
||||
>,也不会进行除自动开聊Boss以外的行为;<b class="color-red">请勿向他人泄漏您的Cookie</b
|
||||
您所在公司可能会对您的计算机终端或网络进行<b color-red>监控</b>,从而<b color-red
|
||||
>审计、跟踪</b
|
||||
>您的行为;上级 / HRBP 团队可能会从 IT
|
||||
团队处获取到监控数据,从而了解团队成员离职倾向。如果您不希望上级 / HRBP
|
||||
团队了解到您正在求职,建议您<b color-red
|
||||
>不要在您所在公司提供的计算机终端或网络上使用本程序</b
|
||||
>。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
|
||||
本程序<b class="color-red"
|
||||
>不会上报您的隐私(诸如能够识别出您身份的信息),不会向您的雇主报告您的求职行为</b
|
||||
>;如果您在使用过程中遇上程序错误,您可以点击程序左下角进行反馈。
|
||||
本程序尊重您的隐私,<b color-red
|
||||
>不会上报能够识别出您身份的信息、不会参与任何钓鱼活动、不会向您所在公司及上级 / HRBP
|
||||
团队报告您的求职行为、不会向猎头公司泄露您的信息</b
|
||||
>。
|
||||
</ElCheckbox>
|
||||
|
||||
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
|
||||
<b class="color-red">本程序不对您的求职过程与结果负责</b
|
||||
>,为您开聊的职位均在Boss直聘上发布;请<b class="color-red">自行甄别为您开聊的公司</b
|
||||
>、<b class="color-red">认真决定是否参加面试</b>、<b class="color-red">慎重选择Offer</b
|
||||
本程序<b class="color-red">没有内置任何付费功能</b>,<b class="color-red"
|
||||
>下载、使用是免费的</b
|
||||
>,任何人可以<b class="color-red">免费获得、免费使用</b>。<b class="color-red"
|
||||
>作者没有利用本程序赚到过任何收入</b
|
||||
>。如果您是从GitHub以外的地方付费后“购买”的本程序,或您被提示“必须付费后才能使用本程序”,<b
|
||||
class="color-red"
|
||||
>那您大概率被骗了</b
|
||||
>,或者<b class="color-red">您下载到了本程序修改版</b>。<b class="color-red"
|
||||
>本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方</b
|
||||
>。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
|
||||
请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b>。
|
||||
本程序<b class="color-red">不对您的求职过程与结果负责</b
|
||||
>,为您开聊的职位均在Boss直聘上发布,职位信息真实性由Boss直聘负责;请<b
|
||||
class="color-red"
|
||||
>自行甄别为您开聊的公司、认真决定是否参加面试、慎重选择Offer</b
|
||||
>。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
|
||||
本程序经历过了多次测试,理论上来说大部分情况下可以正常运行;如果您有顾虑,建议通过VMWare
|
||||
WorkStation/Fusion、VirtualBox、Hyper-V/Windows沙盒等虚拟化技术运行本程序。如发生问题,请点击程序左下角进行反馈。
|
||||
请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
|
||||
>;如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃,请<b
|
||||
class="color-red"
|
||||
>在Boss直聘上屏蔽当前公司及与之关联的公司</b
|
||||
>。
|
||||
</ElCheckbox>
|
||||
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
|
||||
本程序经历过了多次测试,理论上来说大部分情况下可以正常运行,但可能也会出现测试用例覆盖不到位,导致程序不按预期运行的情况;如果您有顾虑,建议通过
|
||||
VMware Workstation / Fusion、Oracle VirtualBox、Microsoft Hyper-V
|
||||
等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况,请点击程序左下角进行反馈。
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</article>
|
||||
@@ -86,26 +112,36 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus'
|
||||
import { ref, onMounted, onBeforeMount } from 'vue';
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { ref, onMounted, onBeforeMount } from 'vue'
|
||||
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
|
||||
const gtagRenderer = (name, params?: object) => {
|
||||
return baseGtagRenderer(name, {
|
||||
scene: 'first-run-agreement',
|
||||
...params
|
||||
})
|
||||
}
|
||||
const electron = window.electron
|
||||
|
||||
const readmeItemCheckStatusList = ref<number[]>([])
|
||||
|
||||
const handleCancel = () => {
|
||||
const handleCancel = async () => {
|
||||
gtagRenderer('cancel_clicked')
|
||||
await sleep(500)
|
||||
electron.ipcRenderer.invoke('exit-app-immediately')
|
||||
}
|
||||
|
||||
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
|
||||
const handleSubmit = () => {
|
||||
gtagRenderer('submit_clicked')
|
||||
const COUNT = 9
|
||||
const COUNT = 10
|
||||
if (readmeItemCheckStatusList.value.length !== COUNT) {
|
||||
gtagRenderer('agreement_not_finish_read_tip_displayed')
|
||||
ElMessage.warning(
|
||||
`您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`
|
||||
)
|
||||
ElMessage.warning({
|
||||
message: `您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`,
|
||||
grouping: true
|
||||
})
|
||||
unreadItemsAfterClickSubmit.value = {}
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
if (!readmeItemCheckStatusList.value.includes(i)) {
|
||||
@@ -127,13 +163,10 @@ const handleReadmeItemCheckStatusListChange = (value: number[]) => {
|
||||
const componentRootEl = ref<HTMLElement>()
|
||||
onMounted(() => {
|
||||
const ro = new ResizeObserver(() => {
|
||||
electron.ipcRenderer.send(
|
||||
'update-window-size',
|
||||
{
|
||||
width: componentRootEl.value!.offsetWidth,
|
||||
height: componentRootEl.value!.offsetHeight,
|
||||
}
|
||||
)
|
||||
electron.ipcRenderer.send('update-window-size', {
|
||||
width: componentRootEl.value!.offsetWidth,
|
||||
height: componentRootEl.value!.offsetHeight
|
||||
})
|
||||
})
|
||||
ro.observe(componentRootEl.value!)
|
||||
onBeforeMount(() => {
|
||||
|
||||
@@ -254,7 +254,7 @@ import {
|
||||
} from 'element-plus'
|
||||
import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue'
|
||||
import { ref, onMounted, watch, nextTick, computed } from 'vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
|
||||
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../../common/constant'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
@@ -267,6 +267,13 @@ interface LlmConfigItem {
|
||||
enabled: true
|
||||
}
|
||||
|
||||
const gtagRenderer = (name, params?: object) => {
|
||||
return baseGtagRenderer(name, {
|
||||
scene: 'llm-config',
|
||||
...params
|
||||
})
|
||||
}
|
||||
|
||||
function getNewConfigItem(): LlmConfigItem {
|
||||
return {
|
||||
id: uuid(),
|
||||
|
||||
@@ -11,8 +11,23 @@
|
||||
<div v-else>
|
||||
<div>当前未选择任何期望城市,将不会按照城市进行筛选</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button size="small" type="primary" @click="isDialogVisible = true">选择城市</el-button>
|
||||
<div
|
||||
line-height-1
|
||||
:style="{
|
||||
marginTop: modelValue?.length ? '10px' : ''
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="
|
||||
() => {
|
||||
isDialogVisible = true
|
||||
gtagRenderer('choose_city_entry_button_clicked')
|
||||
}
|
||||
"
|
||||
>选择城市</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="modelValue?.length"
|
||||
size="small"
|
||||
@@ -105,6 +120,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType, ref } from 'vue'
|
||||
import cityGroupData from '../../../../../../common/constant/cityGroup.json'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -136,23 +152,29 @@ for (const group of cityGroup) {
|
||||
function handleDialogOpen() {
|
||||
activeTabName.value = '热门城市'
|
||||
selectedCities.value = [...(props.modelValue ?? [])]
|
||||
gtagRenderer('choose_city_dialog_open')
|
||||
}
|
||||
|
||||
function handleCancelClicked() {
|
||||
gtagRenderer('choose_city_cancel_button_clicked')
|
||||
isDialogVisible.value = false
|
||||
}
|
||||
function handleConfirmClicked() {
|
||||
gtagRenderer('choose_city_confirm_button_clicked', { value: selectedCities.value.join(',') })
|
||||
isDialogVisible.value = false
|
||||
emits('update:modelValue', [...(selectedCities.value ?? [])])
|
||||
}
|
||||
function handleDialogClosed() {
|
||||
selectedCities.value = []
|
||||
gtagRenderer('choose_city_dialog_closed')
|
||||
}
|
||||
|
||||
function handleClearSelectedCitiesInModelValue() {
|
||||
emits('update:modelValue', [])
|
||||
gtagRenderer('clear_selected_cities_in_mv_clicked')
|
||||
}
|
||||
function handleClearSelectedCitiesInDialog() {
|
||||
selectedCities.value = []
|
||||
gtagRenderer('clear_selected_cities_in_dialog_clicked')
|
||||
}
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,14 @@
|
||||
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
|
||||
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
|
||||
</template>
|
||||
<template v-if="row.markReason === MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT">
|
||||
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
|
||||
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
|
||||
</template>
|
||||
<template v-if="row.markReason === MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT">
|
||||
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
|
||||
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
|
||||
</template>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="experienceName" label="工作经验" />
|
||||
@@ -209,7 +217,9 @@ const markReasonTopicMap = {
|
||||
[MarkAsNotSuitReason.BOSS_INACTIVE]: 'Boss不活跃',
|
||||
[MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON]: '手动标记不合适',
|
||||
[MarkAsNotSuitReason.JOB_NOT_SUIT]: '职位不合适',
|
||||
[MarkAsNotSuitReason.JOB_CITY_NOT_SUIT]: '工作地不合适'
|
||||
[MarkAsNotSuitReason.JOB_CITY_NOT_SUIT]: '工作地不合适',
|
||||
[MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT]: '工作经验不合适',
|
||||
[MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT]: '薪资不合适'
|
||||
}
|
||||
|
||||
function formatMarkReason(row: VMarkAsNotSuitLog) {
|
||||
@@ -241,6 +251,7 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
|
||||
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT: {
|
||||
const extInfo = (() => {
|
||||
try {
|
||||
@@ -253,6 +264,21 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: {
|
||||
const extInfo = (() => {
|
||||
try {
|
||||
return JSON.parse(row.extInfo)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
return [
|
||||
extInfo?.salaryDesc && `薪资:${extInfo.salaryDesc}`,
|
||||
extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
default: {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -231,10 +231,15 @@ import {
|
||||
RECHAT_CONTENT_SOURCE,
|
||||
RECHAT_LLM_FALLBACK
|
||||
} from '../../../../common/enums/auto-start-chat'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
|
||||
import mittBus from '../../utils/mitt'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const gtagRenderer = (name, params?: object) => {
|
||||
return baseGtagRenderer(name, {
|
||||
scene: 'rnrr-config',
|
||||
...params
|
||||
})
|
||||
}
|
||||
const router = useRouter()
|
||||
const formContent = ref({
|
||||
autoReminder: {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 793 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 902 KiB |
@@ -322,11 +322,18 @@
|
||||
import { ElForm, ElButton, ElAlert, ElMessageBox } from 'element-plus'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
|
||||
import { type ResumeContent, resumeContentEnoughDetect } from '../../../../common/utils/resume'
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
|
||||
const gtagRenderer = (name, params?: object) => {
|
||||
return baseGtagRenderer(name, {
|
||||
scene: 'resume-editor',
|
||||
...params
|
||||
})
|
||||
}
|
||||
|
||||
const getEmptyFormContent = () => {
|
||||
const o: any = {
|
||||
expectJob: '',
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -150,6 +150,9 @@ importers:
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
vuedraggable:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(vue@3.4.15)
|
||||
devDependencies:
|
||||
'@electron-toolkit/eslint-config':
|
||||
specifier: ^1.0.2
|
||||
@@ -209,8 +212,8 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(vite@5.1.1)
|
||||
element-plus:
|
||||
specifier: ^2.5.5
|
||||
version: 2.5.5(vue@3.4.15)
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4(vue@3.4.15)
|
||||
eslint:
|
||||
specifier: ^8.56.0
|
||||
version: 8.56.0
|
||||
@@ -3085,6 +3088,11 @@ packages:
|
||||
|
||||
/dayjs@1.11.10:
|
||||
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
|
||||
dev: false
|
||||
|
||||
/dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
dev: true
|
||||
|
||||
/de-indent@1.0.2:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
@@ -3360,8 +3368,8 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/element-plus@2.5.5(vue@3.4.15):
|
||||
resolution: {integrity: sha512-yGU/MruLOWI0ImQPFCyFM4cXtHtILJNAi0hhLImcxjRukjgQLYNkvcvbwqNgBUt808KSeKF9MyxENFyBQLTg+Q==}
|
||||
/element-plus@2.8.4(vue@3.4.15):
|
||||
resolution: {integrity: sha512-ZlVAdUOoJliv4kW3ntWnnSHMT+u/Os7mXJjk2xzOlqNeHaI2/ozlF+R58ZCEak8ZnDi6+5A2viWEYRsq64IuiA==}
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
dependencies:
|
||||
@@ -3373,7 +3381,7 @@ packages:
|
||||
'@types/lodash-es': 4.17.12
|
||||
'@vueuse/core': 9.13.0(vue@3.4.15)
|
||||
async-validator: 4.2.5
|
||||
dayjs: 1.11.10
|
||||
dayjs: 1.11.13
|
||||
escape-html: 1.0.3
|
||||
lodash: 4.17.21
|
||||
lodash-es: 4.17.21
|
||||
@@ -5816,6 +5824,10 @@ packages:
|
||||
ip: 2.0.0
|
||||
smart-buffer: 4.2.0
|
||||
|
||||
/sortablejs@1.14.0:
|
||||
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
|
||||
dev: false
|
||||
|
||||
/source-map-js@1.0.2:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6620,6 +6632,15 @@ packages:
|
||||
'@vue/shared': 3.4.15
|
||||
typescript: 5.3.3
|
||||
|
||||
/vuedraggable@4.1.0(vue@3.4.15):
|
||||
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
|
||||
peerDependencies:
|
||||
vue: ^3.0.1
|
||||
dependencies:
|
||||
sortablejs: 1.14.0
|
||||
vue: 3.4.15(typescript@5.3.3)
|
||||
dev: false
|
||||
|
||||
/web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
Reference in New Issue
Block a user