mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-07-12 07:52:18 +08:00
Compare commits
127 Commits
ui-v0.8.0
...
ui-v0.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98300ddf66 | ||
|
|
962dcb1125 | ||
|
|
a9823f1e24 | ||
|
|
57eb09ed49 | ||
|
|
a8454cbaac | ||
|
|
ddc8a81c7c | ||
|
|
ca37e258c6 | ||
|
|
454ee421bf | ||
|
|
69bd774640 | ||
|
|
20f081a245 | ||
|
|
252869576b | ||
|
|
98ab2acf50 | ||
|
|
dbc127be06 | ||
|
|
82f19d772d | ||
|
|
94de507c9a | ||
|
|
c1faecaaed | ||
|
|
880557c1ad | ||
|
|
3b318262a4 | ||
|
|
f9786d5d82 | ||
|
|
b583951cb5 | ||
|
|
d95abd3ad1 | ||
|
|
2e9d3af276 | ||
|
|
fbeb01ab48 | ||
|
|
6db6ef4972 | ||
|
|
7b3390745a | ||
|
|
b2b1cecfa9 | ||
|
|
61d7d968a1 | ||
|
|
c18930f76e | ||
|
|
fc13742d52 | ||
|
|
af05a5555e | ||
|
|
bc57a07a0a | ||
|
|
265f2f89e5 | ||
|
|
206d50ca61 | ||
|
|
9324f20ad3 | ||
|
|
632a11b105 | ||
|
|
418ca66a30 | ||
|
|
0928b402fb | ||
|
|
373e45dee8 | ||
|
|
8bbfa5b41e | ||
|
|
aa410cfc60 | ||
|
|
e986fddbfb | ||
|
|
cf7a0d3902 | ||
|
|
c95349c037 | ||
|
|
8197782102 | ||
|
|
f17d626ea0 | ||
|
|
2b4b620977 | ||
|
|
03e3c3661f | ||
|
|
5d787fdd50 | ||
|
|
b68ff63b6b | ||
|
|
2aee76307f | ||
|
|
e2040caa38 | ||
|
|
fba45e420f | ||
|
|
fe629eaa22 | ||
|
|
056ec7a0a6 | ||
|
|
f27ca4145a | ||
|
|
cd09210017 | ||
|
|
aec72c28f0 | ||
|
|
69cf5fd34f | ||
|
|
5f47e47448 | ||
|
|
4e9b44d04c | ||
|
|
d206d45242 | ||
|
|
0aef2af350 | ||
|
|
c3dc75b503 | ||
|
|
7a1ae48aa7 | ||
|
|
e5225006cc | ||
|
|
fdf2c61620 | ||
|
|
1f140121fb | ||
|
|
57cf631b86 | ||
|
|
36aef1c16d | ||
|
|
d21972020e | ||
|
|
51ca5dd71d | ||
|
|
e7d5ef382e | ||
|
|
59a36dc7bd | ||
|
|
73b57e21e4 | ||
|
|
6f0d98653c | ||
|
|
3bf6d4c358 | ||
|
|
2c7ecafee2 | ||
|
|
f88a5a2889 | ||
|
|
ac19ae3832 | ||
|
|
a25b047f99 | ||
|
|
42fc66a0f4 | ||
|
|
203fbba9a0 | ||
|
|
b821bc500c | ||
|
|
0032a356e5 | ||
|
|
b9c4976291 | ||
|
|
558891ad9f | ||
|
|
d2da9f5a7d | ||
|
|
71068be21f | ||
|
|
2b0cf2a2fa | ||
|
|
ceaf68e4eb | ||
|
|
10cc248784 | ||
|
|
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 |
7
.github/workflows/release-ui.yml
vendored
7
.github/workflows/release-ui.yml
vendored
@@ -60,9 +60,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- os: macos-13 # for x64 build
|
||||
- os: macos-15-intel # for x64 build
|
||||
arch: x64
|
||||
- os: macos-14 # for arm64 build
|
||||
- os: macos-15 # for arm64 build
|
||||
arch: arm64
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 'true'
|
||||
@@ -149,9 +149,10 @@ jobs:
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -llR geekgeekrun-ui@${{ github.sha }}
|
||||
|
||||
- name: Create release
|
||||
- name: Create release for public
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
repo: geekgeekrun
|
||||
prerelease: true
|
||||
allowUpdates: true
|
||||
artifacts: geekgeekrun-ui@${{ github.sha }}/*
|
||||
|
||||
75
README.md
75
README.md
@@ -1,5 +1,9 @@
|
||||
# 牛人快跑 - GeekGeekRun
|
||||
|
||||
**->由于BOSS直聘于2026.1.8上线了反调试/反爬虫/反自动化逻辑,本程序可能暂时无法使用,目前正在尝试处理中;有相关处理经验的朋友可以提交PR<-**
|
||||
|
||||
**->维护者正在疯狂找工作<-**
|
||||
|
||||
一款可以帮助你在Boss直聘上**自动批量开聊Boss**的脚本,基于Puppeteer。
|
||||
|
||||
与每一位牛人站在一起
|
||||
@@ -12,33 +16,34 @@
|
||||
## 程序有哪些功能?运行逻辑是什么?怎样使用?
|
||||
|
||||
### Boss炸弹
|
||||
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。
|
||||
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
|
||||
|
||||
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ebf5c51c-f52c-4426-a60a-97916f663c6a" />
|
||||
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
|
||||
|
||||
|
||||
#### 匹配步骤
|
||||
1. 按照公司名称查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
||||
2. 检查Boss活跃度
|
||||
- 如果Boss活跃度为本月活跃或更往前的时间,则会把职位标记为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
||||
3. 对职位名称、职位类型、职位描述进行匹配
|
||||
- 如果匹配则自动点击开聊按钮
|
||||
- 不匹配则标记这个职位为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
||||
1. 选择职位来源、职位筛选条件
|
||||
<img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
|
||||
|
||||
2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
||||
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、Boss活跃度进行匹配
|
||||
- 如果可以匹配,则自动点击开聊按钮
|
||||
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
|
||||
- 首先查看是否命中策略为“在Boss直聘上标记不合适”的配置,如果命中则在Boss直聘上点击相关UI,标记不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位(对于“推荐列表中的职位”、“通过搜索找到的职位”,此项会回落到“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”)
|
||||
- 然后查看是否命中策略为“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”的配置,如果命中则仅在本地数据库中记录,且本次运行将无视这个职位
|
||||
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
|
||||
|
||||
#### 异常情况
|
||||
- 当前页面筛选条件下,如果没有更多职位,则自动切换备选筛选条件,以获取更多新职位
|
||||
- 当前页面筛选条件下,如果没有更多可投递职位,则自动切换筛选条件,以获取更多新职位;如果所有筛选条件搜索完没有更多可投递职位,则切换其它职位来源继续查找更多新职位;如果所有职位来源筛选完没有更多可投递职位,则等候一段时间后,会回到第一个职位来源的第一个筛选条件,从头开始查找职位
|
||||
- 如当天开聊次数用完,本程序会暂停运行60分钟,之后尝试继续重新运行;如重新运行时间已在第二天,则将会继续开聊
|
||||
|
||||
#### 运行效果
|
||||
|
||||
> Boss直聘界面已改版,如下是旧版Boss直聘界面
|
||||
|
||||
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 已读不回提醒器
|
||||
Boss不明原因已读不回?简历就是投不出去?
|
||||
|
||||
@@ -118,12 +123,12 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
```
|
||||
1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置
|
||||
|
||||
## 关于软件不能正常启动、不能按预期运行的一些说明(朋友们帮忙测试一下吧)
|
||||
## 关于软件不能正常启动、不能按预期运行的一些说明
|
||||
本程序一直都在不断地更新迭代中,包括新功能的增加以及Bug的修复。
|
||||
|
||||
然而作者本人只有 1台 macOS (Apple Silicon)设备、1台 Windows 设备,且只有作者1人自测(白天工作,晚上熬夜开发、测试),测试条件不是太好(人力不足、设备不足),测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时,就会准出、发版。
|
||||
然而作者本人只有 1台 macOS (Apple Silicon)设备、1台 Windows 设备,且只有作者1人自测(白天工作,晚上熬夜开发、测试),测试条件不是太好(人力不足、设备不足),测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时,即认为可以准出、发版。
|
||||
|
||||
这将不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看,问题也会尝试进行修复。
|
||||
所以不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看,问题也会尝试进行修复。
|
||||
|
||||
如果部分朋友超级喜欢写代码,或者本身工作就是写代码,可以帮忙下载 [Debugtron](https://github.com/pd4d10/debugtron/releases/tag/v1.0.0-alpha.0) 来调试本程序,查看反馈本程序的日志(如下图)。
|
||||
|
||||
@@ -135,10 +140,10 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
### 个人而言
|
||||
以我为例,我真的求职求累了
|
||||
|
||||
2023年初~2025年初,这段时间
|
||||
- 求职经历:离职求职5个月,在职求职3个月;曾面试66个岗位,各种面试(一面、二面、三面、HR面)加起来134次;Offer 寥寥无几
|
||||
- 被动失业经历:1 次裁员,1 次卡试用期
|
||||
- 目前状态:有工作(但愿明天也有工作)
|
||||
2023年初~2026年年初,这段时间
|
||||
- 求职经历:离职求职7个月;曾面试73个岗位,各种面试(一面、二面、三面、HR面)加起来156次;Offer 寥寥无几
|
||||
- 被动失业经历:1 次裁员,1 次卡试用期,1 次部门整体裁撤
|
||||
- 目前状态:无工作(**正在疯狂找工作**)
|
||||
|
||||
如此多的经历,已经让我成为了一位资深Boss用户
|
||||
|
||||
@@ -156,8 +161,8 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场
|
||||
|
||||
当然,如果运气好:
|
||||
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个烂 Offer
|
||||
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了烂 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
|
||||
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被 ** 到,但你又没有其它更好 Offer ,不敢贸然放弃这个不满意的 Offer
|
||||
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了不满意的 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
|
||||
|
||||
为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧
|
||||
|
||||
@@ -175,14 +180,22 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
## 使用必读及免责声明
|
||||
如下是使用必读及免责声明,请您务必逐条阅读;本程序首次启动时,您还会再看到一次;若您不接受如下提到的任何内容,请不要使用本程序。
|
||||
- 本程序从某种程度上说属于辅助工具,与《<a href="https://about.zhipin.com/agreement/?id=registerprotocol_30" rel="noreferer noopener" target="blank">Boss直聘用户协议</a>》(2023年3月版)相关条款相违背,您在注册Boss直聘时已签署过这一条款;根据该条款`七、用户的平台使用义务`、`八、违约责任` 章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于**账号被强制退出登录**、**账号被限制使用**、**账号被封禁**等对您不利的风险;因此使用本程序即意味着**您愿意接受以上风险**,且如果相关风险发生,您需要自行承担相关后果,**本程序概不负责** 。
|
||||
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的Boss直聘账号**进行求职。
|
||||
- 本程序原理是模拟用户在Boss直聘网页上进行点击操作;Boss直聘网站每过一段时间会发生改版,且有可能包含A/B实验,这将导致本程序相关脚本失效。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
||||
- 您的雇主可能会对您的计算机终端或网络进行监控,从而审计、跟踪您的行为;建议您**不要在您雇主提供的计算机终端或网络上使用本程序**。
|
||||
- 本程序需要存储您的登录凭据,即Cookie,来模拟您在Boss直聘上开聊Boss的行为;本程序仅会把您的Cookie存储在本地,并在您访问Boss直聘时将其传输到Boss直聘,**不会泄露给第三方**,也不会进行除自动开聊Boss以外的行为;**请勿向他人泄漏您的Cookie**。
|
||||
- 本程序**不会上报能够识别出您身份的信息**,**不会向您的雇主报告您的求职行为**。
|
||||
- 本程序**不对您的求职过程与结果负责**,为您开聊的职位均在Boss直聘上发布;请**自行甄别为您开聊的公司、认真决定是否参加面试、慎重选择Offer**。
|
||||
- 请在Boss直聘上自行**屏蔽您不期望投递的公司**。
|
||||
- 本程序经历过了多次测试,理论上来说大部分情况下可以正常运行;如果您有顾虑,建议通过VMWare WorkStation/Fusion、VirtualBox、Hyper-V/Windows沙盒等虚拟化技术运行本程序。如发生问题,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
||||
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的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)进行反馈。
|
||||
|
||||
## 想联系作者?想反馈使用体验上的问题?想找求职搭子?
|
||||
|
||||
<img width="360" alt="QQ Group QRCode" src="https://github.com/user-attachments/assets/67560948-467b-44f5-a3e3-c8713255d8b7" />
|
||||
|
||||
|
||||
欢迎进群聊天(刚建的群,人有点少,大胆聊就好)
|
||||
|
||||
--------
|
||||
|
||||
@@ -192,10 +205,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>
|
||||
|
||||
感谢支持
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
"author": "geekgeekrun",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"dayjs": "^1.11.10",
|
||||
"json5": "^2.2.3",
|
||||
"minimist": "^1.2.8",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer": "24.19.0",
|
||||
"puppeteer-extra": "3.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||
"rimraf": "^3.0.2",
|
||||
@@ -28,5 +30,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": "=8.15.9"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"find-chrome-bin@2.0.4": "patches/find-chrome-bin@2.0.4.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
export default {
|
||||
"code": 0,
|
||||
"message": "Success",
|
||||
"zpData": {
|
||||
@@ -56,10 +56,11 @@ function combineWithZero(arr, min, max) {
|
||||
}
|
||||
|
||||
export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
const { salaryList, experienceList, degreeList, scaleList, industryList } =
|
||||
const { cityList, salaryList, experienceList, degreeList, scaleList, industryList } =
|
||||
selectedFilters;
|
||||
|
||||
// 生成符合限制条件的组合
|
||||
const cityComb = combineWithZero(cityList, 0, 1) // Salary: 0-1 个
|
||||
const salaryComb = combineWithZero(salaryList, 0, 1) // Salary: 0-1 个
|
||||
const experienceComb = combineWithZero(experienceList, 0, experienceList.length) // Experience: 0 个或更多
|
||||
const degreeComb = combineWithZero(degreeList, 0, degreeList.length) // Degree: 0 个或更多
|
||||
@@ -67,17 +68,20 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
|
||||
|
||||
// 通过迭代生成所有组合
|
||||
for (const salary of salaryComb) {
|
||||
for (const experience of experienceComb) {
|
||||
for (const degree of degreeComb) {
|
||||
for (const scale of scaleComb) {
|
||||
for (const industry of industryComb) {
|
||||
yield {
|
||||
salaryList: salary,
|
||||
experienceList: experience,
|
||||
degreeList: degree,
|
||||
scaleList: scale,
|
||||
industryList: industry
|
||||
for (const city of cityComb) {
|
||||
for (const salary of salaryComb) {
|
||||
for (const experience of experienceComb) {
|
||||
for (const degree of degreeComb) {
|
||||
for (const scale of scaleComb) {
|
||||
for (const industry of industryComb) {
|
||||
yield {
|
||||
cityList: city,
|
||||
salaryList: salary,
|
||||
experienceList: experience,
|
||||
degreeList: degree,
|
||||
scaleList: scale,
|
||||
industryList: industry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,8 +94,9 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
//#region get count of combinations
|
||||
|
||||
// 计算符合限制条件的组合数量
|
||||
export function calculateTotalCombinations(selectedFilters) {
|
||||
export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) {
|
||||
const {
|
||||
cityList = [],
|
||||
salaryList = [],
|
||||
experienceList = [],
|
||||
degreeList = [],
|
||||
@@ -100,14 +105,70 @@ export function calculateTotalCombinations(selectedFilters) {
|
||||
} = selectedFilters
|
||||
|
||||
// 生成符合限制条件的组合
|
||||
const cityComb = combineWithZero(cityList, 0, 1) // City: 0-1 个
|
||||
const salaryComb = combineWithZero(salaryList, 0, 1) // Salary: 0-1 个
|
||||
const experienceComb = combineWithZero(experienceList, 0, experienceList.length) // Experience: 0 个或更多
|
||||
const degreeComb = combineWithZero(degreeList, 0, degreeList.length) // Degree: 0 个或更多
|
||||
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 = [cityComb, 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
|
||||
})
|
||||
}
|
||||
|
||||
export function getStaticCombineFilterKey(condition) {
|
||||
const kAsO = {}
|
||||
for (const key of Object.keys(condition ?? []).sort()) {
|
||||
if (condition[key] === null || condition[key] === undefined) {
|
||||
continue
|
||||
}
|
||||
kAsO[key] = condition[key]
|
||||
}
|
||||
return JSON.stringify(kAsO)
|
||||
}
|
||||
|
||||
export function formatStaticCombineFilters(rawStaticCombineRecommendJobFilterConditions) {
|
||||
rawStaticCombineRecommendJobFilterConditions = JSON.parse(JSON.stringify(rawStaticCombineRecommendJobFilterConditions))
|
||||
const map = new Map()
|
||||
for (const condition of rawStaticCombineRecommendJobFilterConditions ?? []) {
|
||||
const key = getStaticCombineFilterKey(condition)
|
||||
map.set(key, condition)
|
||||
}
|
||||
const conditions = Array.from(map.values())
|
||||
const result = conditions.map((condition) => {
|
||||
return {
|
||||
cityList: condition.city ? [condition.city] : [],
|
||||
salaryList: condition.salary ? [condition.salary] : [],
|
||||
experienceList: condition.experience ? [condition.experience] : [],
|
||||
degreeList: condition.degree ? [condition.degree] : [],
|
||||
scaleList: condition.scale ? [condition.scale] : [],
|
||||
industryList: condition.industry ? [condition.industry] : []
|
||||
}
|
||||
})
|
||||
if (!result.length) {
|
||||
result.push({
|
||||
cityList: [],
|
||||
salaryList: [],
|
||||
experienceList: [],
|
||||
degreeList: [],
|
||||
scaleList: [],
|
||||
industryList: []
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"combineRecommendJobFilterType": 1,
|
||||
"anyCombineRecommendJobFilter": {
|
||||
"cityList": [],
|
||||
"salaryList": [],
|
||||
"experienceList": [],
|
||||
"degreeList": [],
|
||||
"scaleList": [],
|
||||
"industryList": []
|
||||
},
|
||||
"staticCombineRecommendJobFilterConditions": [],
|
||||
"isSkipEmptyConditionForCombineRecommendJobFilter": false,
|
||||
"expectJobRegExpStr": "",
|
||||
"jobNotMatchStrategy": 1,
|
||||
"jobNotActiveStrategy": 1,
|
||||
@@ -13,6 +17,7 @@
|
||||
"expectCityList": [],
|
||||
"expectCityNotMatchStrategy": 3,
|
||||
"strategyScopeOptionWhenMarkJobCityNotMatch": 2,
|
||||
"jobDetailRegExpMatchLogic": 1,
|
||||
"autoReminder": {
|
||||
"throttleIntervalMinutes": 10,
|
||||
"rechatLimitDay": 21,
|
||||
@@ -21,5 +26,14 @@
|
||||
"recentMessageQuantityForLlm": 8,
|
||||
"rechatLlmFallback": 1,
|
||||
"onlyRemindBossWithExpectJobType": true
|
||||
}
|
||||
},
|
||||
"jobSourceList": [
|
||||
{
|
||||
"type": "expect",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"isSageTimeEnabled": true,
|
||||
"sageTimeOpTimes": 100,
|
||||
"sageTimePauseMinute": 15
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
54
packages/geek-auto-start-chat-with-boss/sage-time.mjs
Normal file
54
packages/geek-auto-start-chat-with-boss/sage-time.mjs
Normal file
@@ -0,0 +1,54 @@
|
||||
import { readConfigFile } from './runtime-file-utils.mjs'
|
||||
import { sleep } from "@geekgeekrun/utils/sleep.mjs"
|
||||
let {
|
||||
isSageTimeEnabled,
|
||||
sageTimeOpTimes,
|
||||
sageTimePauseMinute,
|
||||
} = readConfigFile('boss.json')
|
||||
isSageTimeEnabled = isSageTimeEnabled ?? true
|
||||
sageTimeOpTimes =
|
||||
isNaN(parseInt(sageTimeOpTimes)) ||
|
||||
parseInt(sageTimeOpTimes) < 1
|
||||
? 100
|
||||
: parseInt(sageTimeOpTimes)
|
||||
sageTimePauseMinute =
|
||||
isNaN(parseFloat(sageTimePauseMinute)) ||
|
||||
parseFloat(sageTimePauseMinute) < 0
|
||||
? 15
|
||||
: parseFloat(sageTimePauseMinute)
|
||||
if (parseFloat(sageTimePauseMinute) === 0) {
|
||||
isSageTimeEnabled = false
|
||||
}
|
||||
|
||||
let totalEnabledTimes = 0
|
||||
let recordedOpCount = 0
|
||||
export const waitForSageTimeOrJustContinue = async ({
|
||||
tag,
|
||||
hooks,
|
||||
} = {}) => {
|
||||
if (!isSageTimeEnabled) {
|
||||
return
|
||||
}
|
||||
const tagText = tag ? ` ${tag}` : ''
|
||||
if (recordedOpCount > sageTimeOpTimes) {
|
||||
totalEnabledTimes++
|
||||
console.log(`[SageTime${tagText}] 请求已达限制,开启;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次开启`)
|
||||
await hooks?.sageTimeEnter?.promise({
|
||||
tag,
|
||||
totalEnabledTimes,
|
||||
recordedOpCount,
|
||||
})
|
||||
await sleep(sageTimePauseMinute * 60 * 1000)
|
||||
console.log(`[SageTime${tagText}] 请求限制已解除,关闭;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次关闭`)
|
||||
await hooks?.sageTimeExit?.promise({
|
||||
tag,
|
||||
totalEnabledTimes,
|
||||
recordedOpCount,
|
||||
})
|
||||
recordedOpCount = 0
|
||||
}
|
||||
else {
|
||||
console.log(`[SageTime${tagText}] 请求未达限制;当前记录次数 ${recordedOpCount};已开启过 ${totalEnabledTimes} 次`)
|
||||
recordedOpCount++
|
||||
}
|
||||
}
|
||||
1
packages/laodeng/index.js
Normal file
1
packages/laodeng/index.js
Normal file
File diff suppressed because one or more lines are too long
27
packages/laodeng/package.json
Normal file
27
packages/laodeng/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@geekgeekrun/puppeteer-extra-plugin-laodeng",
|
||||
"version": "0.0.1",
|
||||
"description": "laodeng",
|
||||
"main": "./index.js",
|
||||
"author": "geekgeekrun",
|
||||
"productName": "GeekGeekRun",
|
||||
"homepage": "https://github.com/geekgeekrun",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin": "^3.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright-extra": "*",
|
||||
"puppeteer": "^24.19.0",
|
||||
"puppeteer-extra": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"puppeteer-extra": {
|
||||
"optional": true
|
||||
},
|
||||
"playwright-extra": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,8 @@ export async function main() {
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: [
|
||||
`--load-extension=${editThisCookieExtensionPath}`
|
||||
]
|
||||
pipe: true,
|
||||
enableExtensions: [editThisCookieExtensionPath]
|
||||
})
|
||||
|
||||
const closeAttachedSet = new WeakSet()
|
||||
|
||||
@@ -56,7 +56,10 @@ 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']),
|
||||
sageTimeEnter: new AsyncSeriesHook(['args']),
|
||||
sageTimeExit: 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;
|
||||
}
|
||||
|
||||
15
packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts
Normal file
15
packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { JobHireStatus } from "../enums";
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryColumn } = requireTypeorm();
|
||||
|
||||
@Entity()
|
||||
export class JobHireStatusRecord {
|
||||
@PrimaryColumn()
|
||||
encryptJobId: string;
|
||||
|
||||
@Column()
|
||||
hireStatus: JobHireStatus;
|
||||
|
||||
@Column()
|
||||
lastSeenDate: Date;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -22,4 +22,26 @@ export enum StrategyScopeOptionWhenMarkJobNotMatch {
|
||||
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,
|
||||
}
|
||||
|
||||
export enum CombineRecommendJobFilterType {
|
||||
ANY_COMBINE = 1,
|
||||
STATIC_COMBINE = 2,
|
||||
}
|
||||
|
||||
export enum JobHireStatus {
|
||||
HIRING = 1,
|
||||
CLOSED = 2,
|
||||
DELETED = 3,
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
|
||||
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
|
||||
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
|
||||
import { LlmModelUsageRecord } from "./entity/LlmModelUsageRecord";
|
||||
import { JobHireStatusRecord } from "./entity/JobHireStatusRecord";
|
||||
|
||||
function getBossInfoIfIsEqual (savedOne, currentOne) {
|
||||
if (savedOne === currentOne) {
|
||||
@@ -245,7 +246,7 @@ export async function saveChatStartupRecord(
|
||||
ds: DataSource,
|
||||
_jobInfo,
|
||||
{ encryptUserId },
|
||||
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined } = {}
|
||||
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined, jobSource = undefined } = {}
|
||||
) {
|
||||
const { jobInfo } = _jobInfo;
|
||||
|
||||
@@ -256,7 +257,8 @@ export async function saveChatStartupRecord(
|
||||
encryptCurrentUserId: encryptUserId,
|
||||
encryptJobId: jobInfo.encryptId,
|
||||
autoStartupChatRecordId,
|
||||
chatStartupFrom
|
||||
chatStartupFrom,
|
||||
jobSource,
|
||||
}
|
||||
Object.assign(chatStartupLog, chatStartupLogPayload)
|
||||
|
||||
@@ -270,7 +272,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 +286,8 @@ export async function saveMarkAsNotSuitRecord(
|
||||
markFrom,
|
||||
markReason,
|
||||
extInfo: extInfo ? JSON.stringify(extInfo) : undefined,
|
||||
markOp
|
||||
markOp,
|
||||
jobSource,
|
||||
}
|
||||
Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload)
|
||||
|
||||
@@ -333,9 +336,55 @@ 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
|
||||
}
|
||||
|
||||
export async function saveJobHireStatusRecord(
|
||||
ds: DataSource,
|
||||
record: JobHireStatusRecord
|
||||
) {
|
||||
const jobHireStatusRecordRepository = ds.getRepository(JobHireStatusRecord);
|
||||
await jobHireStatusRecordRepository.save(record);
|
||||
return
|
||||
}
|
||||
|
||||
export async function getJobHireStatusRecord(
|
||||
ds: DataSource,
|
||||
encryptJobId: string
|
||||
) {
|
||||
const repo = ds.getRepository(JobHireStatusRecord)
|
||||
const result = await repo.findOne({
|
||||
where: {
|
||||
encryptJobId
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import "reflect-metadata";
|
||||
import { type DataSource } from "typeorm";
|
||||
import { requireTypeorm } from "./utils/module-loader";
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { BossInfo } from "./entity/BossInfo";
|
||||
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
|
||||
@@ -21,15 +20,27 @@ import { VCompanyLibrary } from "./entity/VCompanyLibrary"
|
||||
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
|
||||
import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
||||
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
||||
import { JobHireStatusRecord } from './entity/JobHireStatusRecord'
|
||||
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord, getNotSuitMarkRecordsInLastSomeDays } from "./handlers";
|
||||
import {
|
||||
saveChatStartupRecord,
|
||||
saveJobInfoFromRecommendPage,
|
||||
saveMarkAsNotSuitRecord,
|
||||
getNotSuitMarkRecordsInLastSomeDays,
|
||||
getChatStartupRecordsInLastSomeDays,
|
||||
getBossIdsByJobIds,
|
||||
saveJobHireStatusRecord
|
||||
} from "./handlers";
|
||||
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
|
||||
import minimist from 'minimist'
|
||||
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
|
||||
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
|
||||
import { JobHireStatus, MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
|
||||
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
|
||||
import { Init1000000000000 } from "./migrations/1000000000000-Init";
|
||||
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
|
||||
import { AddJobHireStatusTable1766466476822 } from "./migrations/1766466476822-AddJobHireStatusTable";
|
||||
const lodashImportPromise = import('lodash-es')
|
||||
|
||||
export function initDb(dbFilePath) {
|
||||
const { DataSource } = requireTypeorm()
|
||||
@@ -59,12 +70,15 @@ export function initDb(dbFilePath) {
|
||||
VMarkAsNotSuitLog,
|
||||
ChatMessageRecord,
|
||||
LlmModelUsageRecord,
|
||||
JobHireStatusRecord,
|
||||
],
|
||||
migrations: [
|
||||
Init1000000000000,
|
||||
UpdateChatStartupLogTable1729182577167,
|
||||
UpdateBossInfoTable1732032381304,
|
||||
AddColumnForMarkAsNotSuitLog1746092370665,
|
||||
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526,
|
||||
AddJobHireStatusTable1766466476822
|
||||
],
|
||||
migrationsRun: true
|
||||
});
|
||||
@@ -111,6 +125,7 @@ export default class SqlitePlugin {
|
||||
expectCityNotMatchStrategy,
|
||||
blockJobNotSuit,
|
||||
blockBossNotActive,
|
||||
blockBossNotNewChat
|
||||
}) => {
|
||||
if (
|
||||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
@@ -118,8 +133,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 +152,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 +165,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 +178,21 @@ export default class SqlitePlugin {
|
||||
id => blockJobNotSuit.add(id)
|
||||
)
|
||||
}
|
||||
const last30DayChatStartupRecords = (await getChatStartupRecordsInLastSomeDays(ds, 30)) ?? [];
|
||||
const chattedJobIds = last30DayChatStartupRecords.map(it => it.encryptJobId)
|
||||
if (chattedJobIds.length === 0) {
|
||||
return
|
||||
}
|
||||
const { chunk } = await lodashImportPromise
|
||||
const chattedJobIdChunks = chunk(chattedJobIds, 200)
|
||||
const chattedBossIds = [];
|
||||
for (const chattedJobIdChunk of chattedJobIdChunks) {
|
||||
const chattedBossIdChunk = ((await getBossIdsByJobIds(ds, chattedJobIdChunk)) ?? []).map(it => it.encryptBossId)
|
||||
chattedBossIds.push(...chattedBossIdChunk)
|
||||
}
|
||||
for (const id of chattedBossIds) {
|
||||
blockBossNotNewChat.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -163,22 +202,33 @@ export default class SqlitePlugin {
|
||||
await saveJobInfoFromRecommendPage(ds, _jobInfo);
|
||||
});
|
||||
|
||||
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList } = {}) => {
|
||||
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async ({ jobInfo }) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
|
||||
autoStartupChatRecordId: this.runRecordId,
|
||||
chatStartupFrom
|
||||
return await saveJobHireStatusRecord(ds, {
|
||||
encryptJobId: jobInfo.encryptId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined } = {}) => {
|
||||
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,
|
||||
jobSource
|
||||
});
|
||||
});
|
||||
|
||||
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> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
export class AddJobHireStatusTable1766466476822 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE IF NOT EXISTS "job_hire_status_record" ("encryptJobId" varchar PRIMARY KEY NOT NULL, "hireStatus" integer NOT NULL, "lastSeenDate" datetime NOT NULL);`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
import { presetUno, presetAttributify, presetIcons } from 'unocss'
|
||||
import transformerDirective from "@unocss/transformer-directives";
|
||||
import transformerDirective from '@unocss/transformer-directives'
|
||||
import Replace from 'unplugin-replace/vite'
|
||||
|
||||
process.env = { ...process.env, ...loadEnv(process.env.NODE_ENV!, process.cwd()) }
|
||||
@@ -13,11 +13,17 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
external: []
|
||||
},
|
||||
minify: 'terser'
|
||||
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
|
||||
},
|
||||
plugins: [
|
||||
externalizeDepsPlugin({
|
||||
exclude: ['@geekgeekrun/geek-auto-start-chat-with-boss', '@geekgeekrun/dingtalk-plugin', '@geekgeekrun/utils', 'find-chrome-bin', '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension']
|
||||
exclude: [
|
||||
'@geekgeekrun/geek-auto-start-chat-with-boss',
|
||||
'@geekgeekrun/dingtalk-plugin',
|
||||
'@geekgeekrun/utils',
|
||||
'find-chrome-bin',
|
||||
'@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
|
||||
]
|
||||
}),
|
||||
Replace({
|
||||
delimiters: ['', ''],
|
||||
@@ -26,11 +32,11 @@ export default defineConfig({
|
||||
values: [
|
||||
{
|
||||
find: /<measurement_id>/g,
|
||||
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string,
|
||||
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string
|
||||
},
|
||||
{
|
||||
find: /<api_secret>/g,
|
||||
replacement: process.env.VITE_APP_GTAG_API_SECRET as string,
|
||||
replacement: process.env.VITE_APP_GTAG_API_SECRET as string
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -39,7 +45,7 @@ export default defineConfig({
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
minify: 'terser'
|
||||
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
|
||||
}
|
||||
},
|
||||
renderer: {
|
||||
@@ -52,11 +58,11 @@ export default defineConfig({
|
||||
vue(),
|
||||
UnoCSS({
|
||||
presets: [presetUno(), presetAttributify(), presetIcons()],
|
||||
transformers: [transformerDirective()],
|
||||
transformers: [transformerDirective()]
|
||||
})
|
||||
],
|
||||
build: {
|
||||
minify: 'terser'
|
||||
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "geekgeekrun-ui",
|
||||
"version": "0.8.0",
|
||||
"version": "0.14.0",
|
||||
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "geekgeekrun",
|
||||
@@ -33,18 +33,21 @@
|
||||
"@geekgeekrun/dingtalk-plugin": "workspace:*",
|
||||
"@geekgeekrun/geek-auto-start-chat-with-boss": "workspace:*",
|
||||
"@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension": "workspace:*",
|
||||
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
|
||||
"@geekgeekrun/sqlite-plugin": "workspace:*",
|
||||
"@geekgeekrun/utils": "workspace:*",
|
||||
"JSONStream": "^1.3.5",
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimist": "^1.2.8",
|
||||
"mitt": "^3.0.1",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"pinia": "^3.0.2",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer": "24.19.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",
|
||||
@@ -52,7 +55,7 @@
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@inquirer/prompts": "^7.0.1",
|
||||
"@inquirer/select": "^4.0.1",
|
||||
"@puppeteer/browsers": "^2.0.0",
|
||||
"@puppeteer/browsers": "^2.11.0",
|
||||
"@rushstack/eslint-patch": "^1.7.1",
|
||||
"@types/node": "^18.19.9",
|
||||
"@unocss/preset-attributify": "^0.58.5",
|
||||
@@ -69,9 +72,8 @@
|
||||
"element-plus": "^2.8.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"find-chrome-bin": "^2.0.1",
|
||||
"find-chrome-bin": "^2.0.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"normalize.css": "^8.0.1",
|
||||
"prettier": "^3.2.4",
|
||||
"sass": "^1.70.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "0.8.0",
|
||||
"buildVersion": 15,
|
||||
"buildTime": 1749349412239,
|
||||
"buildHash": "9b64f05cb6205fc89bd3ad1c08a59993cd6ac127",
|
||||
"version": "0.14.0",
|
||||
"buildVersion": 30,
|
||||
"buildTime": 1769326019101,
|
||||
"buildHash": "962dcb1125db93faff6159b618fb9384c4156e76",
|
||||
"name": "geekgeekrun-ui"
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export enum DOWNLOAD_ERROR_EXIT_CODE {
|
||||
|
||||
export const checkAndDownloadDependenciesForInit = async () => {
|
||||
process.on('disconnect', () => app.exit())
|
||||
process.on('uncaughtException', () => app.exit(DOWNLOAD_ERROR_EXIT_CODE.DOWNLOAD_ERROR))
|
||||
app.dock?.hide()
|
||||
let pipe: null | fs.WriteStream = null
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface BrowserInfo {
|
||||
executablePath: string
|
||||
}
|
||||
|
||||
const CONFIG_VSERION = 2
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
export const lastUsedBrowserRecordFilePath = path.join(
|
||||
runtimeFolderPath,
|
||||
@@ -28,8 +30,13 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
|
||||
}
|
||||
try {
|
||||
const fileContent = (await fsPromise.readFile(lastUsedBrowserRecordFilePath)).toString()
|
||||
const [path, browser] = fileContent.split('\n').map((it) => it.trim())
|
||||
if (!path || !fs.existsSync(path)) {
|
||||
const [path, browser, configVersion] = fileContent.split('\n').map((it) => it.trim())
|
||||
if (
|
||||
!path ||
|
||||
!fs.existsSync(path) ||
|
||||
!Number(configVersion) ||
|
||||
Number(configVersion) < CONFIG_VSERION
|
||||
) {
|
||||
await removeLastUsedAndAvailableBrowserPath()
|
||||
return null
|
||||
}
|
||||
@@ -57,7 +64,7 @@ export const saveLastUsedAndAvailableBrowserInfo = async (browserInfo: BrowserIn
|
||||
}
|
||||
await fsPromise.writeFile(
|
||||
lastUsedBrowserRecordFilePath,
|
||||
[browserInfo.executablePath, browserInfo.browser].join('\n')
|
||||
[browserInfo.executablePath, browserInfo.browser, CONFIG_VSERION].join('\n')
|
||||
)
|
||||
} catch {
|
||||
console.warn('lastUsedBrowserRecordFile write error')
|
||||
|
||||
@@ -16,7 +16,7 @@ const getPuppeteerManagerModule = async () => {
|
||||
return puppeteerManager
|
||||
}
|
||||
|
||||
const EXPECT_CHROMIUM_BUILD_ID = '113.0.5672.63'
|
||||
const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
|
||||
const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')
|
||||
|
||||
const getExpectCachedPuppeteerExecutable = async (): Promise<BrowserInfo> => {
|
||||
@@ -130,7 +130,7 @@ export async function findAndLocateUserInstalledChromiumExecutableSync(): Promis
|
||||
const findChrome: typeof import('find-chrome-bin').findChrome = (await import('find-chrome-bin'))
|
||||
.findChrome
|
||||
const targetBrowser = await findChrome({
|
||||
min: exceptChromiumMainVersion
|
||||
min: exceptChromiumMainVersion + 1
|
||||
})
|
||||
if (!targetBrowser?.executablePath) {
|
||||
throw new Error('NO_EXPECT_CHROMIUM_FOUND')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import DingtalkPlugin from '@geekgeekrun/dingtalk-plugin/index.mjs'
|
||||
import { app } from 'electron'
|
||||
import { app, dialog } from 'electron'
|
||||
import { SyncHook, AsyncSeriesHook } from 'tapable'
|
||||
import {
|
||||
readConfigFile,
|
||||
@@ -93,7 +93,10 @@ 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']),
|
||||
sageTimeEnter: new AsyncSeriesHook(['args']),
|
||||
sageTimeExit: new AsyncSeriesHook(['args'])
|
||||
}
|
||||
initPlugins(hooks)
|
||||
|
||||
@@ -120,6 +123,11 @@ const runAutoChat = async () => {
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('LOGIN_STATUS_INVALID')) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `登录状态无效`,
|
||||
detail: `请重新登录Boss直聘`
|
||||
})
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -4,16 +4,26 @@ 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,
|
||||
saveChatStartupRecord,
|
||||
saveMarkAsNotSuitRecord,
|
||||
saveChatMessageRecord
|
||||
saveChatMessageRecord,
|
||||
saveJobHireStatusRecord
|
||||
} 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,
|
||||
JobHireStatus
|
||||
} from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import { Target } from 'puppeteer'
|
||||
@@ -39,13 +49,136 @@ const attachRequestsListener = async (target: Target) => {
|
||||
return
|
||||
}
|
||||
|
||||
function handleJobDetailPage({ encryptJobId } = { encryptJobId: null }) {
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
if (!encryptJobId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
({ encryptJobId }) => {
|
||||
return (
|
||||
location.href.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
|
||||
document.readyState === 'complete'
|
||||
)
|
||||
},
|
||||
undefined,
|
||||
{ encryptJobId }
|
||||
)
|
||||
const htmlContent = await page.content()
|
||||
if (htmlContent) {
|
||||
const $ = cheerio.load(htmlContent)
|
||||
const [jobBannerEl] = $('#main .job-banner') ?? []
|
||||
if (!jobBannerEl) {
|
||||
console.log(`access might be blocked`)
|
||||
if (
|
||||
htmlContent.includes(`您访问的页面不存在`) ||
|
||||
location.href === `https://www.zhipin.com/`
|
||||
) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.DELETED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
|
||||
if (jobStatusTextEl) {
|
||||
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
|
||||
if ([`职位已关闭`].includes(jobStatusText)) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.CLOSED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
} else {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
})
|
||||
.catch(() => void 0)
|
||||
}
|
||||
|
||||
if (page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
|
||||
const encryptJobId = page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
|
||||
if (encryptJobId) {
|
||||
handleJobDetailPage({ encryptJobId })
|
||||
}
|
||||
}
|
||||
|
||||
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')) {
|
||||
if (response.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
|
||||
const encryptJobId = response
|
||||
.url()
|
||||
.match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
|
||||
if (encryptJobId) {
|
||||
handleJobDetailPage({ encryptJobId })
|
||||
}
|
||||
} else if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
|
||||
const data = await response.json()
|
||||
|
||||
console.log(data)
|
||||
if (data.code === 0) {
|
||||
await saveJobInfoFromRecommendPage(await dbInitPromise, data.zpData)
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId: data.zpData.jobInfo.encryptId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/reasons.json')
|
||||
@@ -81,7 +214,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 +223,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 +266,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 +279,8 @@ const attachRequestsListener = async (target: Target) => {
|
||||
encryptUserId: currentUserInfo.encryptUserId
|
||||
},
|
||||
{
|
||||
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList
|
||||
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
|
||||
jobSource: JobSource[jobSource]
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
@@ -191,11 +329,15 @@ const attachRequestsListener = async (target: Target) => {
|
||||
)
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const chatRecordList = rawChatRecordList.map(it => {
|
||||
const chatRecordList = rawChatRecordList.map((it) => {
|
||||
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
|
||||
mappedItem.mid = it.mid
|
||||
mappedItem.encryptFromUserId = it.isSelf ? currentUserInfo.encryptUserId : bossInfo.encryptBossId
|
||||
mappedItem.encryptToUserId = it.isSelf ? bossInfo.encryptBossId : currentUserInfo.encryptUserId
|
||||
mappedItem.encryptFromUserId = it.isSelf
|
||||
? currentUserInfo.encryptUserId
|
||||
: bossInfo.encryptBossId
|
||||
mappedItem.encryptToUserId = it.isSelf
|
||||
? bossInfo.encryptBossId
|
||||
: currentUserInfo.encryptUserId
|
||||
mappedItem.style = it.isSelf ? 'sent' : 'received'
|
||||
mappedItem.type = it.type
|
||||
mappedItem.time = it.time ? new Date(it.time) : null
|
||||
@@ -229,7 +371,8 @@ export async function launchBossSite() {
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: [`--load-extension=${editThisCookieExtensionPath}`]
|
||||
pipe: true,
|
||||
enableExtensions: [editThisCookieExtensionPath]
|
||||
})
|
||||
let [page] = await browser.pages()
|
||||
for (let i = 0; i < bossCookies.length; i++) {
|
||||
|
||||
@@ -149,6 +149,32 @@ export default function initIpc() {
|
||||
bossConfig.strategyScopeOptionWhenMarkJobWorkExpNotMatch =
|
||||
payload.strategyScopeOptionWhenMarkJobWorkExpNotMatch
|
||||
}
|
||||
if (hasOwn(payload, 'jobDetailRegExpMatchLogic')) {
|
||||
bossConfig.jobDetailRegExpMatchLogic = payload.jobDetailRegExpMatchLogic
|
||||
}
|
||||
if (hasOwn(payload, 'isSkipEmptyConditionForCombineRecommendJobFilter')) {
|
||||
bossConfig.isSkipEmptyConditionForCombineRecommendJobFilter =
|
||||
payload.isSkipEmptyConditionForCombineRecommendJobFilter
|
||||
}
|
||||
if (hasOwn(payload, 'jobSourceList')) {
|
||||
bossConfig.jobSourceList = payload.jobSourceList
|
||||
}
|
||||
if (hasOwn(payload, 'combineRecommendJobFilterType')) {
|
||||
bossConfig.combineRecommendJobFilterType = payload.combineRecommendJobFilterType
|
||||
}
|
||||
if (hasOwn(payload, 'staticCombineRecommendJobFilterConditions')) {
|
||||
bossConfig.staticCombineRecommendJobFilterConditions =
|
||||
payload.staticCombineRecommendJobFilterConditions
|
||||
}
|
||||
if (hasOwn(payload, 'isSageTimeEnabled')) {
|
||||
bossConfig.isSageTimeEnabled = payload.isSageTimeEnabled
|
||||
}
|
||||
if (hasOwn(payload, 'sageTimeOpTimes')) {
|
||||
bossConfig.sageTimeOpTimes = payload.sageTimeOpTimes
|
||||
}
|
||||
if (hasOwn(payload, 'sageTimePauseMinute')) {
|
||||
bossConfig.sageTimePauseMinute = payload.sageTimePauseMinute
|
||||
}
|
||||
|
||||
promiseArr.push(writeConfigFile('boss.json', bossConfig))
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ export async function launchBoss(browser: Browser) {
|
||||
}
|
||||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
|
||||
try {
|
||||
await Promise.all([page.goto(bossChatUiUrl, { timeout: 0 }), page.waitForNavigation()])
|
||||
await Promise.all([
|
||||
page.goto(bossChatUiUrl, { timeout: 0 }),
|
||||
page.waitForNavigation({ timeout: 120 * 1000 })
|
||||
])
|
||||
} catch (error) {
|
||||
if (error?.message?.startsWith('net::ERR_INTERNET_DISCONNECTED')) {
|
||||
throw new Error('ERR_INTERNET_DISCONNECTED')
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MsgStatus, type ChatListItem } from './types'
|
||||
import { Browser, Page } from 'puppeteer'
|
||||
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
|
||||
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
|
||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||
import { app, dialog } from 'electron'
|
||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||
@@ -11,7 +12,11 @@ import {
|
||||
readConfigFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatMessageRecord'
|
||||
import { saveChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
import {
|
||||
saveChatMessageRecord,
|
||||
getJobHireStatusRecord,
|
||||
saveJobHireStatusRecord
|
||||
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
import { writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import * as fs from 'fs'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
@@ -19,6 +24,9 @@ import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||
import { RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat'
|
||||
import gtag from '../../utils/gtag'
|
||||
import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums';
|
||||
import dayjs from 'dayjs'
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
const throttleIntervalMinutes =
|
||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||
@@ -104,6 +112,88 @@ async function saveCurrentChatRecord(page) {
|
||||
await saveChatMessageRecord(ds, chatRecordList)
|
||||
}
|
||||
|
||||
async function checkJobIsClosed() {
|
||||
const encryptJobId = await pageMapByName.boss!.evaluate(() => {
|
||||
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$.encryptJobId
|
||||
})
|
||||
if (!encryptJobId) {
|
||||
return false
|
||||
}
|
||||
let isJobClosed = false
|
||||
let record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
|
||||
// not seen before, or last seen more than 6 hours ago
|
||||
// fetch new status
|
||||
if (
|
||||
!record ||
|
||||
(record.hireStatus === JobHireStatus.HIRING &&
|
||||
Date.now() - Number(dayjs(record.lastSeenDate)) > 6 * 60 * 60 * 1000)
|
||||
) {
|
||||
const positionNameElHandle = await pageMapByName.boss!.$(
|
||||
`#main .chat-conversation [ka="geek_chat_job_detail"] .right-content`
|
||||
)
|
||||
if (!positionNameElHandle) {
|
||||
return false
|
||||
}
|
||||
positionNameElHandle.click()
|
||||
try {
|
||||
const targetPage = await waitForPage(pageMapByName.boss!.browser(), async (page) => {
|
||||
const url = page.url()
|
||||
if (
|
||||
url.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
|
||||
(await page.evaluate(() => document.readyState === 'complete'))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
const htmlContent = await targetPage.content()
|
||||
if (htmlContent) {
|
||||
const $ = cheerio.load(htmlContent)
|
||||
const [jobBannerEl] = $('#main .job-banner') ?? []
|
||||
if (!jobBannerEl) {
|
||||
console.log(`access might be blocked`)
|
||||
if (
|
||||
htmlContent.includes(`您访问的页面不存在`) ||
|
||||
location.href === `https://www.zhipin.com/`
|
||||
) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.DELETED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
|
||||
if (jobStatusTextEl) {
|
||||
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
|
||||
if ([`职位已关闭`].includes(jobStatusText)) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.CLOSED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
} else {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
targetPage.close().catch(() => void 0)
|
||||
} catch (err) {
|
||||
console.log(`checkJobIsClosed error: ${err}`)
|
||||
}
|
||||
record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
|
||||
}
|
||||
if (record.hireStatus !== JobHireStatus.HIRING) {
|
||||
isJobClosed = true
|
||||
}
|
||||
return isJobClosed
|
||||
}
|
||||
|
||||
let browser: null | Browser = null
|
||||
const mainLoop = async () => {
|
||||
if (browser) {
|
||||
@@ -201,7 +291,9 @@ const mainLoop = async () => {
|
||||
(rechatLimitDay && it.updateTime
|
||||
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
|
||||
: true) &&
|
||||
((((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) ||
|
||||
((((it.lastIsSelf &&
|
||||
it.lastMsgStatus === MsgStatus.HAS_READ &&
|
||||
!it.lastText.includes('你撤回了')) ||
|
||||
canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) &&
|
||||
!it.unreadCount) ||
|
||||
(!it.lastIsSelf && it.lastText === '开场问题,期待你的回答'))
|
||||
@@ -301,7 +393,9 @@ const mainLoop = async () => {
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0
|
||||
const isJobClosed = await checkJobIsClosed()
|
||||
if (
|
||||
!isJobClosed &&
|
||||
isExpectJobTypeMatch &&
|
||||
historyMessageList[historyMessageList.length - 1].isSelf &&
|
||||
historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ &&
|
||||
|
||||
@@ -2,26 +2,40 @@ 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 })
|
||||
})
|
||||
hooks.sageTimeEnter.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
|
||||
gtag('sage_time_enter', { tag, totalEnabledTimes, recordedOpCount })
|
||||
})
|
||||
hooks.sageTimeExit.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
|
||||
gtag('sage_time_exit', { tag, totalEnabledTimes, recordedOpCount })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
<template>
|
||||
<div class="job-combo-filter">
|
||||
<div class="filter-item">
|
||||
<div font-size-12px>城市</div>
|
||||
<div
|
||||
style="
|
||||
align-items: center;
|
||||
background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
background-image: none;
|
||||
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
|
||||
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
|
||||
"
|
||||
pl4px
|
||||
pr4px
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
>
|
||||
<city-chooser v-model="modelValue.cityList">
|
||||
<template #default="{ showDialog }">
|
||||
<div flex justify-between items-center>
|
||||
<div font-size-12px>
|
||||
<template v-if="modelValue.cityList?.length"
|
||||
>已选择<span ml3px mr3px>{{ modelValue.cityList?.length }}</span
|
||||
>个城市</template
|
||||
>
|
||||
<template v-else><i color-gray>未选择城市</i></template>
|
||||
</div>
|
||||
<el-button size="small" @click="showDialog" pl4px pr4px>选择</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</city-chooser>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<div font-size-12px>薪资待遇</div>
|
||||
<el-select
|
||||
@@ -10,7 +42,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.salaryList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.salaryList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -27,7 +59,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.experienceList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.experienceList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -44,7 +76,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.degreeList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.degreeList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -84,7 +116,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.scaleList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.scaleList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -97,6 +129,7 @@
|
||||
<script lang="ts" setup>
|
||||
import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-conditions-20241002.json'
|
||||
import industryFilterExemption from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||||
import CityChooser from '@renderer/page/MainLayout/GeekAutoStartChatWithBoss/components/CityChooser.vue'
|
||||
import { PropType } from 'vue'
|
||||
|
||||
defineProps({
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button p-0 h-auto flex size="small" type="text" :icon="Plus" @click="addCondition"
|
||||
>添加条件</el-button
|
||||
>
|
||||
<div class="job-combo-filter" mt-8px>
|
||||
<el-table
|
||||
size="small"
|
||||
:data="props.modelValue"
|
||||
border
|
||||
:style="{ maxWidth: '100%' }"
|
||||
:row-style="
|
||||
({ row }) => {
|
||||
return {
|
||||
backgroundColor:
|
||||
duplicatedMap.get(getStaticCombineFilterKey(row))?.length > 1
|
||||
? '#fcd4b7'
|
||||
: 'transparent'
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #empty>
|
||||
<div lh-1.5em>
|
||||
列表中没有条件,将仅使用默认的“初始空条件”为您筛选职位<br />
|
||||
你可以点击表格左上角“<el-button
|
||||
p-0
|
||||
h-auto
|
||||
size="small"
|
||||
type="text"
|
||||
:icon="Plus"
|
||||
@click="addCondition"
|
||||
>添加条件</el-button
|
||||
>”按钮,添加更多筛选条件。
|
||||
</div>
|
||||
</template>
|
||||
<el-table-column :resizable="false" label="" :width="80">
|
||||
<template #default="{ $index: index }">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '2px',
|
||||
height: 'fit-content'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveConditionUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= modelValue?.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveConditionDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="城市" prop="city">
|
||||
<template #default="{ row }">
|
||||
<city-chooser v-model="row.city" :multiple="false">
|
||||
<template #default="{ showDialog }">
|
||||
<div flex justify-between items-center>
|
||||
<div font-size-12px lh-1.2em>
|
||||
<template v-if="row.city">{{ row.city }}</template>
|
||||
<template v-else><i color-gray>未选择城市</i></template>
|
||||
</div>
|
||||
<el-button size="small" @click="showDialog" pl4px pr4px>选择</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</city-chooser>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="薪资待遇" prop="salary">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.salary"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.salaryList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="工作经验" prop="experience">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.experience"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.experienceList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="学历要求" prop="degree">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.degree"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.degreeList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="公司行业" :width="200" prop="industry">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.industry"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option-group
|
||||
v-for="group in industryFilterExemption"
|
||||
:key="group.code"
|
||||
:label="group.name"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in group.subLevelModelList"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
/>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="公司规模" prop="scale">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.scale"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.scaleList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
Array.from(duplicatedMap.values()).some((it) => {
|
||||
return it.length > 1
|
||||
})
|
||||
"
|
||||
color-orange
|
||||
font-size-12px
|
||||
>
|
||||
列表中被橙色高亮的条件存在重复项,相关重复项将被合并,运行时遍历顺序以第一次出现为准
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-conditions-20241002.json'
|
||||
import industryFilterExemption from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||||
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { computed, PropType } from 'vue'
|
||||
import CityChooser from '@renderer/page/MainLayout/GeekAutoStartChatWithBoss/components/CityChooser.vue'
|
||||
|
||||
import { getStaticCombineFilterKey } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<
|
||||
Array<{
|
||||
salary: number | null
|
||||
experience: number | null
|
||||
degree: number | null
|
||||
industry: number | null
|
||||
scale: number | null
|
||||
}>
|
||||
>,
|
||||
default: () => []
|
||||
},
|
||||
isSkipEmptyConditionForCombineRecommendJobFilter: {
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
// fix for misspell of scale property.
|
||||
for (const condition of props.modelValue) {
|
||||
if ((condition as any).scaleList && !condition.scale) {
|
||||
condition.scale = (condition as any).scaleList
|
||||
delete (condition as any).scaleList
|
||||
}
|
||||
}
|
||||
|
||||
function getNewConditionItem() {
|
||||
return {
|
||||
salary: null,
|
||||
experience: null,
|
||||
degree: null,
|
||||
industry: null,
|
||||
scale: null
|
||||
}
|
||||
}
|
||||
|
||||
function addCondition() {
|
||||
props.modelValue?.push(getNewConditionItem())
|
||||
// gtagRenderer('resume_work_exp_added')
|
||||
}
|
||||
function moveConditionUp(index) {
|
||||
;[props.modelValue[index], props.modelValue[index - 1]] = [
|
||||
props.modelValue[index - 1],
|
||||
props.modelValue[index]
|
||||
]
|
||||
// gtagRenderer('resume_work_exp_moved_up')
|
||||
}
|
||||
|
||||
function moveConditionDown(index) {
|
||||
;[props.modelValue[index], props.modelValue[index + 1]] = [
|
||||
props.modelValue[index + 1],
|
||||
props.modelValue[index]
|
||||
]
|
||||
// gtagRenderer('resume_work_exp_moved_down')
|
||||
}
|
||||
|
||||
function removeCondition(index) {
|
||||
props.modelValue?.splice(index, 1)
|
||||
// gtagRenderer('resume_work_exp_removed')
|
||||
}
|
||||
const duplicatedMap = computed(() => {
|
||||
const map = new Map()
|
||||
for (const condition of props.modelValue ?? []) {
|
||||
const key = getStaticCombineFilterKey(condition)
|
||||
if (!map.has(key)) {
|
||||
map.set(key, [])
|
||||
}
|
||||
const arr = map.get(key)
|
||||
arr.push(condition)
|
||||
}
|
||||
return map
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.job-combo-filter {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,9 +11,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onUnmounted, PropType } from 'vue'
|
||||
import { ref, onUnmounted, PropType, h } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import FailMessage from './FailMessage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
dependenciesStatus: {
|
||||
@@ -36,12 +38,24 @@ const downloadProcessExitCode = ref(0)
|
||||
const processDownloadBrowser = async () => {
|
||||
downloadProcessExitCode.value = 0
|
||||
browserDownloadPercentage.value = 0
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('setup-dependencies')
|
||||
browserDownloadPercentage.value = 100
|
||||
} catch (err) {
|
||||
downloadProcessExitCode.value = 1
|
||||
throw err
|
||||
let restRetriedTime = 2
|
||||
while (restRetriedTime > 0) {
|
||||
try {
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('setup-dependencies')
|
||||
browserDownloadPercentage.value = 100
|
||||
} catch (err) {
|
||||
downloadProcessExitCode.value = 1
|
||||
throw err
|
||||
}
|
||||
break
|
||||
} catch (err) {
|
||||
restRetriedTime--
|
||||
if (restRetriedTime === 0) {
|
||||
throw err
|
||||
}
|
||||
await sleep(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +82,13 @@ const processTasks = async () => {
|
||||
await p
|
||||
} catch {
|
||||
gtagRenderer('encounter_error_when_download_deps')
|
||||
await ElMessageBox.confirm('需要重试吗?', '核心组件下载失败', {
|
||||
await ElMessageBox.confirm(h(FailMessage), {
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
showClose: false,
|
||||
type: 'error',
|
||||
cancelButtonText: '退出程序'
|
||||
cancelButtonText: '退出程序',
|
||||
confirmButtonText: '重试'
|
||||
})
|
||||
.then(() => {
|
||||
gtagRenderer('start_retry_download_deps')
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>核心组件下载失败,请重试。</p>
|
||||
<br />
|
||||
<br />
|
||||
<p>
|
||||
<b text-orange>提示:</b>由于网络颠簸,如果多次重试仍然失败,请 <el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
font-size-14px
|
||||
@click="handleOpenChromeDownloadPage"
|
||||
>点击此处</el-button
|
||||
> 下载最新版本 Google Chrome
|
||||
浏览器,安装完毕后,重新打开本程序,程序会自动检测该浏览器并使用它。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import debounce from 'lodash-es/debounce'
|
||||
const { ipcRenderer } = electron
|
||||
|
||||
const handleOpenChromeDownloadPage = debounce(
|
||||
async () => {
|
||||
gtagRenderer('open_chrome_download_page_clicked')
|
||||
ipcRenderer.send('open-external-link', 'https://www.google.cn/chrome/')
|
||||
},
|
||||
1000,
|
||||
{ leading: true, trailing: false }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped></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(),
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="modelValue?.length">
|
||||
<div>当前已选择城市:</div>
|
||||
<div flex flex-wrap gap-10px>
|
||||
<el-tag v-for="it in modelValue" :key="it">
|
||||
{{ it }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>当前未选择任何期望城市,将不会按照城市进行筛选</div>
|
||||
</div>
|
||||
<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"
|
||||
type="danger"
|
||||
@click="handleClearSelectedCitiesInModelValue"
|
||||
>清空已选择的所有城市</el-button
|
||||
>
|
||||
</div>
|
||||
<div w-full>
|
||||
<slot
|
||||
:model-value="modelValue"
|
||||
:show-dialog="() => (isDialogVisible = true)"
|
||||
:clear-value="handleClearSelectedCitiesInModelValue"
|
||||
></slot>
|
||||
<el-dialog
|
||||
v-model="isDialogVisible"
|
||||
width="1000px"
|
||||
title="请选择城市"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
@open="handleDialogOpen"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
@@ -50,7 +20,7 @@
|
||||
label="热门城市"
|
||||
name="热门城市"
|
||||
>
|
||||
<el-checkbox-group v-model="selectedCities">
|
||||
<el-checkbox-group v-if="multiple" v-model="selectedCities">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
@@ -66,6 +36,23 @@
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<el-radio-group v-else v-model="selectedCities" w-full>
|
||||
<div
|
||||
w-full
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-radio
|
||||
v-for="op in hotCityList.filter((it) => it.code !== 100010000)"
|
||||
:key="op.code"
|
||||
:label="op.name"
|
||||
>
|
||||
{{ op.name }}
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-for="it in cityGroupsByAlphabetMap.keys()"
|
||||
@@ -75,8 +62,8 @@
|
||||
:value="it"
|
||||
>
|
||||
<div v-for="group in cityGroupsByAlphabetMap.get(it)" :key="group.firstChar">
|
||||
{{ group.firstChar }}
|
||||
<el-checkbox-group v-model="selectedCities">
|
||||
<div pt4px pb4px>{{ group.firstChar }}</div>
|
||||
<el-checkbox-group v-if="multiple" v-model="selectedCities">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
@@ -88,6 +75,19 @@
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<el-radio-group v-else v-model="selectedCities" w-full>
|
||||
<div
|
||||
w-full
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-radio v-for="op in group.cityList" :key="op.code" :label="op.name">
|
||||
{{ op.name }}
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -101,7 +101,7 @@
|
||||
>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="selectedCities.length"
|
||||
v-if="selectedCities?.length"
|
||||
type="danger"
|
||||
@click="handleClearSelectedCitiesInDialog"
|
||||
>清空已选择的所有城市</el-button
|
||||
@@ -119,12 +119,18 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, ref } from 'vue'
|
||||
import cityGroupData from '../../../../../../common/constant/cityGroup.json'
|
||||
import cityGroupData from '@geekgeekrun/geek-auto-start-chat-with-boss/cityGroup.mjs'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { ElRadioGroup } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<string[]>
|
||||
type: [Array, String] as PropType<string[] | string | null>,
|
||||
default: null
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
@@ -132,7 +138,7 @@ const { hotCityList, cityGroup } = cityGroupData.zpData
|
||||
|
||||
const activeTabName = ref('热门城市')
|
||||
const isDialogVisible = ref(false)
|
||||
const selectedCities = ref([])
|
||||
const selectedCities = ref(null)
|
||||
|
||||
const cityGroupsByAlphabetMap = ref(
|
||||
new Map(['ABCDE', 'FGHJ', 'KLMN', 'PQRST', 'WXYZ'].map((it) => [it, []]))
|
||||
@@ -151,7 +157,7 @@ for (const group of cityGroup) {
|
||||
|
||||
function handleDialogOpen() {
|
||||
activeTabName.value = '热门城市'
|
||||
selectedCities.value = [...(props.modelValue ?? [])]
|
||||
selectedCities.value = props.multiple ? [...(props.modelValue ?? [])] : props.modelValue
|
||||
gtagRenderer('choose_city_dialog_open')
|
||||
}
|
||||
|
||||
@@ -160,21 +166,28 @@ function handleCancelClicked() {
|
||||
isDialogVisible.value = false
|
||||
}
|
||||
function handleConfirmClicked() {
|
||||
gtagRenderer('choose_city_confirm_button_clicked', { value: selectedCities.value.join(',') })
|
||||
gtagRenderer('choose_city_confirm_button_clicked', {
|
||||
value: Array.isArray(selectedCities.value)
|
||||
? selectedCities.value.join(',')
|
||||
: selectedCities.value
|
||||
})
|
||||
isDialogVisible.value = false
|
||||
emits('update:modelValue', [...(selectedCities.value ?? [])])
|
||||
emits(
|
||||
'update:modelValue',
|
||||
props.multiple ? [...(selectedCities.value ?? [])] : selectedCities.value
|
||||
)
|
||||
}
|
||||
function handleDialogClosed() {
|
||||
selectedCities.value = []
|
||||
selectedCities.value = props.multiple ? [] : null
|
||||
gtagRenderer('choose_city_dialog_closed')
|
||||
}
|
||||
|
||||
function handleClearSelectedCitiesInModelValue() {
|
||||
emits('update:modelValue', [])
|
||||
emits('update:modelValue', (selectedCities.value = props.multiple ? [] : null))
|
||||
gtagRenderer('clear_selected_cities_in_mv_clicked')
|
||||
}
|
||||
function handleClearSelectedCitiesInDialog() {
|
||||
selectedCities.value = []
|
||||
selectedCities.value = props.multiple ? [] : null
|
||||
gtagRenderer('clear_selected_cities_in_dialog_clicked')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import { JobDetailRegExpMatchLogic } from '@geekgeekrun/sqlite-plugin/src/enums'
|
||||
|
||||
const expectJobFilterTemplateList = [
|
||||
{
|
||||
type: '不限职位',
|
||||
name: '不限职位(随便投)',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '互联网/AI',
|
||||
name: 'Java',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '\\bJava\\b',
|
||||
expectJobDescRegExpStr:
|
||||
'\\bJava\\b|JVM|消息队列|MQ|SQL|Oracle|MongoDB|Redis|Nginx|Dubbo|Docker|K8s|Kubernetes',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '互联网/AI',
|
||||
name: 'Golang',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '\\bGolang\\b',
|
||||
expectJobDescRegExpStr:
|
||||
'\\bGo\\b|\\bGolang\\b|消息队列|MQ|SQL|Oracle|MongoDB|Redis|Nginx|Dubbo|Docker|K8s|Kubernetes',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '互联网/AI',
|
||||
name: '前端开发工程师',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '前端|H5|\\bFE\\b',
|
||||
expectJobTypeRegExpStr: '前端开发|javascript',
|
||||
expectJobDescRegExpStr: '前端|vue|react|node|\\bjs\\b|javascript|H5',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '互联网/AI',
|
||||
name: '前端开发工程师(不考虑外包、兼职)',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '^(?=.*(前端|H5|\\bFE\\b))(?!.*(?:外包|驻场|外派|兼职|短期))',
|
||||
expectJobTypeRegExpStr: '前端开发|javascript',
|
||||
expectJobDescRegExpStr:
|
||||
'^(?=.*(前端|vue|react|node|\\bjs\\b|javascript|H5))(?!.*(?:外包|驻场|外派|兼职|短期))',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '互联网/AI',
|
||||
name: '测试工程师、测试开发',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '测试|测开|QA|质量',
|
||||
expectJobTypeRegExpStr: '测试工程师|测试开发',
|
||||
expectJobDescRegExpStr:
|
||||
'测试|测开|QA|线上问题|自动化|复盘|效率|Selenium|Puppeteer|Playwright|Cypress|JMeter|LoadRunner|QTP|TestNG|JUnit|Pytest|Fiddler|Charles|Jenkins|Appium|黑盒|白盒|用例|缺陷|Linux|Ubuntu|Debian|CentOS|Shell|c\\+\\+|Python|PHP|\\bJava\\b|Node|\\bGo\\b|\\bGolang\\b',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '互联网/AI',
|
||||
name: '运维工程师、运维开发工程师',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '运维(开发)?|SRE',
|
||||
expectJobTypeRegExpStr: '运维(开发)?工程师',
|
||||
expectJobDescRegExpStr:
|
||||
'运维|SRE|服务器|云计算|Docker|K8s|Kubernetes|Linux|Ubuntu|Debian|CentOS|Shell|Python|\\bGo\\b|\\bGolang\\b|监控|Prometheus|Grafana|ELK|负载均衡|部署|Nginx|Apache|DevOps|harbor',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '互联网/AI',
|
||||
name: '数据开发',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '数据开发',
|
||||
expectJobDescRegExpStr: 'c\\+\\+|Python|\\bGo\\b|\\bGolang\\b|\\bJava\\b|Node|数据仓库|ETL|大数据|Hadoop|Spark|Flink|Hive|Presto|数据湖|数仓|SQL|Oracle|MongoDB|Redis|Kafka',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '互联网/AI',
|
||||
name: '实施工程师',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '实施',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '产品',
|
||||
name: '产品经理',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '产品经理',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '产品',
|
||||
name: '用户研究',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '用户研究',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '产品',
|
||||
name: '游戏策划',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '游戏策划',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '客服/运营',
|
||||
name: '产品运营',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '产品运营',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '客服/运营',
|
||||
name: '用户运营',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '用户运营',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '客服/运营',
|
||||
name: '数据标注/AI训练师',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '数据标注|AI训练师',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '财务/审计/税务',
|
||||
name: '会计、出纳',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '会计|Accountant|出纳|财务',
|
||||
expectJobTypeRegExpStr: '会计|出纳',
|
||||
expectJobDescRegExpStr:
|
||||
'会计|财务|出纳|审计|账务|税务|总账|做账|应付|应收|成本|资产|资金|记账|发票|结算|核算|汇算|利润|对账|报税|回款|SAP|用友|金蝶',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '人力/行政/法务',
|
||||
name: '人力资源专员/助理、人力资源经理/主管',
|
||||
config: {
|
||||
expectJobNameRegExpStr: 'HR|人力|人资|人事',
|
||||
expectJobTypeRegExpStr: '人力',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '人力/行政/法务',
|
||||
name: 'HRBP',
|
||||
config: {
|
||||
expectJobNameRegExpStr: 'BP|HRG|HR|人力|人资|人事',
|
||||
expectJobTypeRegExpStr: 'HRBP|人力',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '人力/行政/法务',
|
||||
name: '员工关系',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '员工关系|劳动关系|SSC|社保|HR|人力|人资|人事',
|
||||
expectJobTypeRegExpStr: '员工关系|人力',
|
||||
expectJobDescRegExpStr: '员工关系|劳动关系|SSC|社保|考勤|入职|离职|入转调离',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '人力/行政/法务',
|
||||
name: '招聘',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '招聘|高招|Recruiter|HR|人力|人资|人事',
|
||||
expectJobTypeRegExpStr: '招聘|猎头|人力',
|
||||
expectJobDescRegExpStr:
|
||||
'招聘|高招|Recruiter|简历|面试|人才引进|Mapping|人才画像|offer|猎头|内推|外推|猎聘|Boss|拉勾|前程无忧|智联|58同城|领英|LinkedIn|ATS|人才库|Moka|北森|iTenant|倍罗|大易',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '人力/行政/法务',
|
||||
name: '薪酬绩效',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '薪酬|绩效|福利|COE|payroll',
|
||||
expectJobTypeRegExpStr: '薪酬绩效',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '人力/行政/法务',
|
||||
name: '企业文化',
|
||||
config: {
|
||||
expectJobNameRegExpStr: '企业文化|组织文化|组织|OC|廉洁|反腐',
|
||||
expectJobTypeRegExpStr: '企业文化',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
export default expectJobFilterTemplateList
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h100vh">
|
||||
<div class="flex flex-col w200px pt30px pl30px aside-nav of-hidden">
|
||||
<div class="flex flex-col min-w200px w200px pt30px pl30px aside-nav of-hidden">
|
||||
<div class="nav-list flex-1 of-auto">
|
||||
<RouterLink to="./GeekAutoStartChatWithBoss">
|
||||
Boss炸弹
|
||||
|
||||
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: '',
|
||||
|
||||
32
packages/utils/puppeteer/wait.mjs
Normal file
32
packages/utils/puppeteer/wait.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
export async function waitForPage(browser, predicate, {
|
||||
polling = 1000, timeout = 10 * 1000
|
||||
} = {}) {
|
||||
let intervalTimer = null
|
||||
let timeoutTimer = null
|
||||
return await new Promise((resolve, reject) => {
|
||||
timeoutTimer = setTimeout(
|
||||
() => {
|
||||
clearInterval(intervalTimer)
|
||||
clearInterval(timeoutTimer)
|
||||
reject(
|
||||
new Error(`waitForPage timeout after ${timeout} ms`)
|
||||
)
|
||||
},
|
||||
timeout
|
||||
)
|
||||
intervalTimer = setInterval(async () => {
|
||||
const allPages = await browser.pages()
|
||||
let target = null
|
||||
for (let i = allPages.length - 1; i >= 0; i--) {
|
||||
const currentPage = allPages[i]
|
||||
if (
|
||||
await predicate(currentPage)
|
||||
) {
|
||||
clearInterval(intervalTimer)
|
||||
clearInterval(timeoutTimer)
|
||||
resolve(currentPage)
|
||||
}
|
||||
}
|
||||
}, polling)
|
||||
})
|
||||
}
|
||||
23
patches/find-chrome-bin@2.0.4.patch
Normal file
23
patches/find-chrome-bin@2.0.4.patch
Normal file
@@ -0,0 +1,23 @@
|
||||
diff --git a/src/win32/index.js b/src/win32/index.js
|
||||
index 53e2279e4a0c51dfe0725100005fafed2c6537a5..d9de7840e2ad0bfe625e6067cc764cee220035f1 100644
|
||||
--- a/src/win32/index.js
|
||||
+++ b/src/win32/index.js
|
||||
@@ -15,12 +15,13 @@ export function findChromeBinaryOnWin32(canary) {
|
||||
].filter(Boolean)
|
||||
|
||||
let result
|
||||
-
|
||||
- prefixes.forEach(prefix => {
|
||||
+ for (const prefix of prefixes) {
|
||||
let chromePath = join(prefix, suffix)
|
||||
- if (canAccess(chromePath)) result = chromePath
|
||||
- })
|
||||
-
|
||||
+ if (canAccess(chromePath)) {
|
||||
+ result = chromePath
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
return result
|
||||
}
|
||||
|
||||
768
pnpm-lock.yaml
generated
768
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user