Compare commits

...

60 Commits

Author SHA1 Message Date
geekgeekrun
7e4f044909 enhance display in mark as not suit record 2026-02-10 06:09:53 +08:00
geekgeekrun
a1f8b6c6f0 consume add block company config in ghosting reminder and auto start chat 2026-02-10 06:09:21 +08:00
geekgeekrun
c0326605a9 remove debugger 2026-02-10 05:30:35 +08:00
geekgeekrun
0460cb331d add block company config field in ui of ghosting reminder 2026-02-10 04:06:49 +08:00
geekgeekrun
85acb599d5 add block company config field in ui of auto start chat 2026-02-10 04:06:28 +08:00
geekgeekrun
f88e295138 enhance ui text 2026-02-09 20:53:35 +08:00
geekgeekrun
81ba666e27 enhance of aside nav ui with box-shadow 2026-02-09 12:09:05 +08:00
geekgeekrun
e33d77c57c change the default value of expectCompanies 2026-02-09 11:39:11 +08:00
geekgeekrun
1d8e99303d enhance ui text about 已读不回自动复聊 2026-02-09 11:20:08 +08:00
geekgeekrun
baef48752f enhance side nav ui 2026-02-09 08:38:03 +08:00
geekgeekrun
46c0bcba42 replace Boss in ui with BOSS 2026-02-09 08:14:39 +08:00
geekgeekrun
6ab40f759a move public setting entry to left side nav; rename feature ui text 2026-02-09 07:52:33 +08:00
geekgeekrun
bfb4fa6e8d enhance ux 2026-02-09 07:11:53 +08:00
geekgeekrun
37ad84ff46 add loading status for auto detect browser 2026-02-09 00:03:53 +08:00
geekgeekrun
3077a0f68f add browser check and process continue run logic 2026-02-08 22:47:51 +08:00
geekgeekrun
68170e8b60 add pre-run-check handler 2026-02-08 19:39:37 +08:00
geekgeekrun
c298904d25 fix when worker process exited, ui no reflection 2026-02-08 18:22:12 +08:00
geekgeekrun
3eeea2e5c7 don't open start editthiscookie start page 2026-02-08 02:22:36 +08:00
geekgeekrun
e746bf70e9 add browser auto find logic to first run 2026-02-08 01:58:44 +08:00
geekgeekrun
8cc94baa18 add executable file basic check 2026-02-08 00:11:07 +08:00
geekgeekrun
3f2b709097 add browserDownloadProgress relevant logic 2026-02-07 23:34:50 +08:00
geekgeekrun
ebf512c86a add browser assistant entry on ui 2026-02-07 20:04:49 +08:00
geekgeekrun
9f97622da9 Merge branch 'master' into feature/multi-process 2026-02-07 04:53:51 +08:00
geekgeekrun
e2ed83c421 Merge remote-tracking branch 'origin/master' into feature/multi-process 2026-02-07 04:43:52 +08:00
geekgeekrun
8105f9933a add socketToWorkerIdSetMap to fix when user process exit, worker is still running; add isReset for ensureIpcPipeName to fix daemon may not be connected 2026-02-07 04:43:19 +08:00
geekgeekrun
f2cc87d660 make daemon keep running in background 2026-02-06 22:02:25 +08:00
geekgeekrun
dcf4706eb6 Merge remote-tracking branch 'origin/feature/ui' into feature/multi-process 2026-02-06 05:45:11 +08:00
geekgeekrun
f04d3843be Merge branch 'master' into feature/multi-process 2026-01-26 22:13:25 +08:00
geekgeekrun
5d46607e0c Merge branch 'master' into feature/multi-process 2026-01-26 21:46:18 +08:00
geekgeekrun
c5789679ba refactor login logic in ghosting reminder 2026-01-24 05:08:04 +08:00
geekgeekrun
204e15b5d8 enhance ux login flow of auto start chat 2026-01-24 04:26:51 +08:00
geekgeekrun
60dee6f435 migrate cookie assistant logic to standalone window make it promisify, and call it with ghosting remainder; disable pre-check when program start run. 2026-01-23 14:37:52 +08:00
geekgeekrun
5ab7fc2ee2 remove period screenshot 2026-01-22 13:24:43 +08:00
geekgeekrun
afe8a820c1 change sqlite backend; upgrade electron; fix ipc file cannot listen in macOS 2026-01-21 02:56:42 +08:00
geekgeekrun
c23d590052 pin pnpm version 2026-01-21 01:42:49 +08:00
geekgeekrun
0b50d2767a fix ipc file cannot listen in linux 2026-01-21 01:07:11 +08:00
geekgeekrun
a1f6dceedb hide task manager entry 2026-01-18 13:15:20 +08:00
geekgeekrun
f09db784fa enhance ux of stop button 2026-01-18 12:35:58 +08:00
geekgeekrun
0df3174ad9 visualize prerequisite check steps 2026-01-18 03:43:58 +08:00
geekgeekrun
ce32dc4c2f move find browser logic into util process 2026-01-17 23:32:19 +08:00
geekgeekrun
351f30b824 move stop button into dialog 2026-01-17 22:53:40 +08:00
geekgeekrun
754f942c86 file rename 2026-01-17 18:27:18 +08:00
geekgeekrun
5063f9c2aa fix screenshot error when page closed; catch EPIPE globally; rigister gui process 2026-01-17 18:07:50 +08:00
geekgeekrun
9639151fdd make task can be run standalone via arg 2026-01-17 15:37:54 +08:00
geekgeekrun
4cf6a7e36f use file instead of port to listen server 2026-01-17 12:36:03 +08:00
geekgeekrun
a05c73cbdf enhance daemon lifecycle logic like connect ensurance 2026-01-17 11:34:28 +08:00
geekgeekrun
199a472ec3 change config of externalizeDepsPlugin 2026-01-17 11:34:22 +08:00
geekgeekrun
5634ac0260 remove wrong import way 2026-01-17 11:34:13 +08:00
geekgeekrun
55a04bb1ad store job info when response 2026-01-16 06:11:35 +08:00
geekgeekrun
492538e663 don't make browser to be like windows; check if page closed before push screenshot 2026-01-15 17:20:10 +08:00
geekgeekrun
6f02160f28 add and patch AnonymizeUaPlugin; make version of find-chrome-bin fixed 2026-01-15 13:17:51 +08:00
geekgeekrun
860847963b add screenshot show 2026-01-15 12:46:16 +08:00
geekgeekrun
69ec879967 add page to show running task 2026-01-14 17:44:42 +08:00
geekgeekrun
45397bcc74 Merge branch 'feature/ui' into feature/multi-process 2026-01-14 10:26:01 +08:00
geekgeekrun
933caaff2a add run-record-id arg 2026-01-08 00:07:59 +08:00
geekgeekrun
8739a70277 migrate daemon to new daemon 2026-01-07 23:21:05 +08:00
geekgeekrun
18af99f10e sendResponse pass callbackUuid directly 2026-01-04 10:54:51 +08:00
geekgeekrun
81e40230d1 WIP: migrate to mulit process manager 2026-01-04 10:32:43 +08:00
geekgeekrun
b730901775 launch daemon in main process 2025-12-31 15:15:24 +08:00
geekgeekrun
f4ca2ebbbd add @geekgeekrun/pm 2025-12-29 23:38:37 +08:00
99 changed files with 5777 additions and 2928 deletions

View File

@@ -2,7 +2,7 @@
**->维护者正在疯狂求职中<-**
一款可以帮助你在Boss直聘上**自动批量开聊Boss**的脚本基于Puppeteer。
一款可以帮助你在BOSS直聘上**自动批量开聊BOSS**的脚本基于Puppeteer。
与每一位牛人站在一起
@@ -16,8 +16,8 @@
## 程序有哪些功能?运行逻辑是什么?怎样使用?
### Boss炸弹
扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的Boss。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
### 自动开聊
扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的BOSS。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
@@ -27,10 +27,10 @@
<img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、Boss活跃度进行匹配
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、BOSS活跃度进行匹配
- 如果可以匹配,则自动点击开聊按钮
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
- 首先查看是否命中策略为“在Boss直聘上标记不合适”的配置如果命中则在Boss直聘上点击相关UI标记不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位对于“推荐列表中的职位”、“通过搜索找到的职位”此项会回落到“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”
- 首先查看是否命中策略为“在BOSS直聘上标记不合适”的配置如果命中则在BOSS直聘上点击相关UI标记不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位对于“推荐列表中的职位”、“通过搜索找到的职位”此项会回落到“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”
- 然后查看是否命中策略为“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”的配置如果命中则仅在本地数据库中记录且本次运行将无视这个职位
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
@@ -40,23 +40,23 @@
#### 运行效果
> Boss直聘界面已改版如下是旧版Boss直聘界面
> BOSS直聘界面已改版如下是旧版BOSS直聘界面
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
### 已读不回提醒器
Boss不明原因已读不回?简历就是投不出去?
### 已读不回自动复聊
BOSS不明原因已读不回?简历就是投不出去?
已读不回提醒器,有事没事提醒一下已读不回的 Ta助力把握每次机会
已读不回自动复聊,有事没事提醒一下已读不回的 Ta助力沟通双向奔赴
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/51a43b0f-118d-4b0a-957c-40a5367e29e4" />
#### 匹配逻辑
1. 在聊天列表中查找对你消息已读不回的Boss,再发一条消息,多次复聊;同时:
1. 在聊天列表中查找对你消息已读不回的BOSS,再发一条消息,多次复聊;同时:
- 如果设置了“跟进时限”,那么在这个时间之前活跃的聊天将不会被检查
- 如果设置了“跟进间隔”且再次检查时发现Boss已读不回,且距离上次提醒时间间隔小于这个时间,那么聊天将暂时不会跟进,直到下次检查时距离上次提醒时间间隔大于这个时间
- 如果设置了“跟进间隔”且再次检查时发现BOSS已读不回,且距离上次提醒时间间隔小于这个时间,那么聊天将暂时不会跟进,直到下次检查时距离上次提醒时间间隔大于这个时间
#### 发送内容
以下二者之一:
@@ -64,8 +64,8 @@ Boss不明原因已读不回简历就是投不出去
2. 由大语言模型(根据简历及当前聊天上下文)生成的内容
## 辅助功能介绍
### Boss 登录助手
帮你用十分简单的方式,登录到 Boss 直聘以进行自动开聊、已读不回提醒避免和乱七八糟的技术名词类似Cookie、登录凭证、JSON……打交道
### BOSS 登录助手
帮你用十分简单的方式,登录到 BOSS 直聘以进行自动开聊、已读不回提醒避免和乱七八糟的技术名词类似Cookie、登录凭证、JSON……打交道
<img width="1680" alt="image" src="https://github.com/user-attachments/assets/b5d02385-f1c0-4045-b01a-66c5f747fec5" />
@@ -76,20 +76,20 @@ Boss不明原因已读不回简历就是投不出去
<img width="576" alt="image" src="https://github.com/user-attachments/assets/ef91642f-b0b0-438f-8c66-e650f0de139b" />
### 已读不回提醒器提示词模板编辑
### 已读不回自动复聊提示词模板编辑
对生成的提醒消息不够满意?您不妨可以自己进行编辑提示词,来获得您期望的结果
<img width="1139" alt="image" src="https://github.com/user-attachments/assets/db956573-61c0-44d4-b588-5c04fe941f84" />
### 已读不回提醒器提醒过程模拟
### 已读不回自动复聊提醒过程模拟
想要测试配置的某一款模型可不可用?想测试使用当前配置生成的内容效果如何?那就来这里试试吧
<img width="600" alt="image" src="https://github.com/user-attachments/assets/3b2776bd-ae5b-48d1-a01c-0819509d96a1" />
### 配置模板选项
初次见面,不会配置本程序?当心配置出现问题?本程序为一些关键的功能配置(例如:Boss炸弹中的期望公司设置、职位详情筛选设置;大语言模型设置)加入了配置模板,让本程序更易用,助你轻松完成配置,开始求职。不妨来试试吧
初次见面,不会配置本程序?当心配置出现问题?本程序为一些关键的功能配置(例如:自动开聊中的期望公司设置、职位详情筛选设置;大语言模型设置)加入了配置模板,让本程序更易用,助你轻松完成配置,开始求职。不妨来试试吧
<img width="576" alt="image" src="https://github.com/user-attachments/assets/2cccfece-17e7-4cc2-bf33-67f0ce500625" />
@@ -138,25 +138,25 @@ Boss不明原因已读不回简历就是投不出去
在此先谢过了~
## 为什么要写这个程序?
根据日常使用求职平台Boss直聘)的经验,在已经开聊很多职位的情况下,经常会推荐:
根据日常使用求职平台BOSS直聘)的经验,在已经开聊很多职位的情况下,经常会推荐:
1. 一些长时间不活跃的“僵尸”职位 - 活跃信息默认隐藏,需要点开职位详情才能看到
2. 牛头不对马嘴,不符合求职期望的职位
人工筛选这些职位会有很大的心智负担,真的会吐……
因此,我在Boss炸弹代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位。
因此,我在自动开聊代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位。
## 使用必读及免责声明
如下是使用必读及免责声明,请您务必逐条阅读;本程序首次启动时,您还会再看到一次;若您不接受如下提到的任何内容,请不要使用本程序。
- 本程序从某种程度上说属于辅助工具,与《<a href="https://about.zhipin.com/agreement/?id=registerprotocol_30" rel="noreferer noopener" target="blank">Boss直聘用户协议</a>》2023年3月版相关条款相违背您在注册Boss直聘时已签署过这一条款;根据该条款`七、用户的平台使用义务`、`八、违约责任` 章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于**账号被强制退出登录**、**账号被限制使用**、**账号被封禁**等对您不利的风险;因此使用本程序即意味着**您愿意接受以上风险**,且如果相关风险发生,您需要自行承担相关后果,**本程序概不负责** 。
- 本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘,**不会泄露给第三方**也不会进行除自动开聊Boss以外的行为;**请勿向他人泄漏您的Cookie**。
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的Boss直聘账号**进行求职。
- 本程序原理是模拟用户在Boss直聘网页上寻找关键元素并进行点击操作Boss直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
- 本程序从某种程度上说属于辅助工具,与《<a href="https://about.zhipin.com/agreement/?id=registerprotocol_30" rel="noreferer noopener" target="blank">BOSS直聘用户协议</a>》2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款;根据该条款`七、用户的平台使用义务`、`八、违约责任` 章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于**账号被强制退出登录**、**账号被限制使用**、**账号被封禁**等对您不利的风险;因此使用本程序即意味着**您愿意接受以上风险**,且如果相关风险发生,您需要自行承担相关后果,**本程序概不负责** 。
- 本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘,**不会泄露给第三方**也不会进行除自动开聊BOSS以外的行为;**请勿向他人泄漏您的Cookie**。
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的BOSS直聘账号**进行求职。
- 本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
- 您所在公司可能会对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;上级/HRBP团队可能会从 IT 团队处获取到监控数据,从而了解团队成员离职倾向。如果您不希望上级/HRBP团队了解到您正在求职建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及上级/HRBP报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**。
- 本程序**没有内置任何付费功能****下载**、**使用**是**免费**的,任何人可以**免费获得**、**免费使用**。**作者没有利用本程序赚到过任何收入**。如果您是从GitHub以外的地方付费后“购买”的本程序或您被提示“必须付费后才能使用本程序”那**您大概率被骗了**,或者**您下载到了本程序修改版**。**本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方**。
- 本程序**不对您的求职过程与结果负责**为您开聊的职位均在Boss直聘上发布职位信息真实性由Boss直聘负责;请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
- 请在Boss直聘上自行**屏蔽您不期望投递的公司**如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃,请**在Boss直聘上屏蔽当前公司及与之关联的公司**。
- 本程序**不对您的求职过程与结果负责**为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责;请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
- 请在BOSS直聘上自行**屏蔽您不期望投递的公司**如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃,请**在BOSS直聘上屏蔽当前公司及与之关联的公司**。
- 本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过VMware Workstation/Fusion、Oracle VirtualBox、Microsoft Hyper-V等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
## 想联系作者?想反馈使用体验上的问题?想找求职搭子?

View File

@@ -18,6 +18,7 @@
"minimist": "^1.2.8",
"puppeteer": "24.19.0",
"puppeteer-extra": "3.3.6",
"puppeteer-extra-plugin-anonymize-ua": "2.4.6",
"puppeteer-extra-plugin-stealth": "2.11.2",
"rimraf": "^3.0.2",
"tapable": "^2.2.1"
@@ -29,11 +30,12 @@
"node": "20.16.0"
},
"engines": {
"pnpm": "=8.15.9"
"pnpm": ">=8.15.9 <9.0.0"
},
"pnpm": {
"patchedDependencies": {
"find-chrome-bin@2.0.4": "patches/find-chrome-bin@2.0.4.patch"
"find-chrome-bin@2.0.4": "patches/find-chrome-bin@2.0.4.patch",
"puppeteer-extra-plugin-anonymize-ua@2.4.6": "patches/puppeteer-extra-plugin-anonymize-ua@2.4.6.patch"
}
}
}

View File

@@ -35,5 +35,6 @@
],
"isSageTimeEnabled": true,
"sageTimeOpTimes": 100,
"sageTimePauseMinute": 15
"sageTimePauseMinute": 15,
"blockCompanyNameRegExpStr": ""
}

View File

@@ -0,0 +1,4 @@
[
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","巨量引擎","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","小鹅通","富途","京东","沃东天骏","达达","达冠","百度","昆仑芯","小度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","智图","长地万方","瑞图万方","道道通","小熊博望","理想","蔚来","顺丰","丰巢","中通","圆通","申通","跨越","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","淘宝","淘麦郎","天猫","盒马","口碑","优视","夸克","UC","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","51talk","爱学习","高思","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","从鲸","TEMU","得物","有赞","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","虾皮","首汽租车","GoFun","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","锐捷","Tenda","腾达","斐讯","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手","Grab","爱回收","洋钱罐","瓴岳","得到","思维造物","地平线","咪咕","翼支付","电信","天翼","联通","蓝湖","墨刀","海尔","美的","米哈游","传音","同花顺","国美","TCL"
]

View File

@@ -1,4 +1 @@
[
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","巨量引擎","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","小鹅通","富途","京东","沃东天骏","达达","达冠","百度","昆仑芯","小度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","智图","长地万方","瑞图万方","道道通","小熊博望","理想","蔚来","顺丰","丰巢","中通","圆通","申通","跨越","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","淘宝","淘麦郎","天猫","盒马","口碑","优视","夸克","UC","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","51talk","爱学习","高思","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","从鲸","TEMU","得物","有赞","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","虾皮","首汽租车","GoFun","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","锐捷","Tenda","腾达","斐讯","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手","Grab","爱回收","洋钱罐","瓴岳","得到","思维造物","地平线","咪咕","翼支付","电信","天翼","联通","蓝湖","墨刀","海尔","美的","米哈游","传音","同花顺","国美","TCL"
]
[]

View File

@@ -86,31 +86,32 @@ export const autoStartChatEventBus = new EventEmitter()
let puppeteer
let StealthPlugin
let LaodengPlugin
let AnonymizeUaPlugin
export async function initPuppeteer () {
// production
const importResult = await Promise.all(
[
import('puppeteer-extra'),
import('puppeteer-extra-plugin-stealth'),
import('@geekgeekrun/puppeteer-extra-plugin-laodeng')
import('@geekgeekrun/puppeteer-extra-plugin-laodeng'),
import('puppeteer-extra-plugin-anonymize-ua')
]
)
puppeteer = importResult[0].default
StealthPlugin = importResult[1].default
LaodengPlugin = importResult[2].default
AnonymizeUaPlugin = importResult[3].default
puppeteer.use(StealthPlugin())
puppeteer.use(LaodengPlugin())
puppeteer.use(AnonymizeUaPlugin({ makeWindows: false }))
return {
puppeteer,
StealthPlugin,
LaodengPlugin
LaodengPlugin,
AnonymizeUaPlugin
}
}
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
const targetCompanyList = readConfigFile('target-company-list.json').filter(it => !!it.trim());
const combineRecommendJobFilterType = readConfigFile('boss.json').combineRecommendJobFilterType ?? CombineRecommendJobFilterType.ANY_COMBINE
@@ -246,6 +247,19 @@ const recommendJobPageUrl = `https://www.zhipin.com/web/geek/jobs`
const expectCompanySet = new Set(targetCompanyList)
const enableCompanyAllowList = Boolean(expectCompanySet.size)
const blockCompanyNameRegExpStr = readConfigFile('boss.json').blockCompanyNameRegExpStr ?? ''
const blockCompanyNameRegExp = (() => {
if (!blockCompanyNameRegExpStr?.trim()) {
return null
}
try {
return new RegExp(blockCompanyNameRegExpStr, 'im')
}
catch {
return null
}
})()
const blockCompanyNameRegMatchStrategy = readConfigFile('boss.json').blockCompanyNameRegMatchStrategy ?? MarkAsNotSuitOp.NO_OP
/**
* @type { import('puppeteer').Browser }
@@ -297,6 +311,18 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
let isOptionChosen = false
if (chooseReasonDialogProxy) {
switch (reasonCode) {
case MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT: {
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title*="公司"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="薪资"]`))
if (opProxy) {
await opProxy.click()
isOptionChosen = true
}
break
}
case MarkAsNotSuitReason.BOSS_INACTIVE: {
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title="BOSS活跃度低"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="职位停招/招满"]`))
@@ -311,7 +337,7 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT: {
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="城市"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="公司"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
@@ -324,7 +350,7 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: {
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title*="薪资"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title$="城市"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="距离"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title*="公司"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="面试过/入职过"]`))
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="重复推荐"]`))
@@ -983,6 +1009,14 @@ async function toRecommendPage (hooks) {
].includes(expectSalaryNotMatchStrategy) &&
strategyScopeOptionWhenMarkSalaryNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
) ? !checkIfSalarySuit(it.salaryDesc) : false
) || (
// enter job detail to mark as not suit for company name filter
!!blockCompanyNameRegExp &&
blockCompanyNameRegExp.test(it.brandName?.toLowerCase?.() ?? '') &&
[
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
].includes(blockCompanyNameRegMatchStrategy)
)
)
})
@@ -1076,6 +1110,47 @@ async function toRecommendPage (hooks) {
//#region collect not suit reasons
const notSuitReasonIdToStrategyMap = {}
const notSuitConditionHandleMap = {
async companyName() {
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
if (blockCompanyNameRegMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL && !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
}
}
else if (blockCompanyNameRegMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
try {
await waitForSageTimeOrJustContinue({
tag: 'beforeJobNotSuitMarked',
hooks
})
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT)
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT,
extInfo: {
chosenReasonInUi
},
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch(err) {
console.log(`mark boss inactive failed`, err)
}
}
},
async active() {
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
@@ -1288,6 +1363,11 @@ async function toRecommendPage (hooks) {
}
}
if (
!!blockCompanyNameRegExp && blockCompanyNameRegExp.test(selectedJobData.brandName ?? '')
) {
notSuitReasonIdToStrategyMap.companyName = blockCompanyNameRegMatchStrategy
}
//#region
// null
// 刚刚活跃 // 今日活跃 // 昨日活跃 // 3日内活跃 // 本周活跃 // 2周内活跃
@@ -1544,10 +1624,13 @@ export async function mainLoop (hooks) {
height: 900 - 140,
}
})
hooks.puppeteerLaunched?.call()
hooks.puppeteerLaunched?.call(browser)
page = (await browser.pages())[0]
hooks.pageGotten?.call(page)
//set cookies
hooks.cookieWillSet?.call(bossCookies)
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
await hooks.cookieWillSet?.promise(bossCookies)
for(let i = 0; i < bossCookies.length; i++){
await page.setCookie(bossCookies[i]);
}
@@ -1576,7 +1659,10 @@ export async function closeBrowserWindow () {
browser?.close()
const browserProcess = browser?.process()
if (browserProcess) {
process.kill(browserProcess.pid)
try {
process.kill(browserProcess.pid)
}
catch {}
}
browser = null
page = null

View File

@@ -32,25 +32,20 @@ async function getEditThisCookieZipPath () {
return editThisCookieZipPath
}
const APP_GEEKGEEKRUN_EDIT_VERSION = 1
export async function ensureEditThisCookie () {
let isNeedExtractEditThisCookie = false
const manifestFilePath = path.join(editThisCookieExtensionPath, 'manifest.json')
if (!fs.existsSync(
manifestFilePath
)) {
const GEEKGEEKRUN_EDIT_VERSION_FILE_PATH = path.join(editThisCookieExtensionPath, 'GEEKGEEKRUN_EDIT_VERSION')
let geekgeekrunEditVersion
try {
const fileContent = fs.readFileSync(GEEKGEEKRUN_EDIT_VERSION_FILE_PATH, { encoding: 'utf-8' })
geekgeekrunEditVersion = Number(fileContent) || 0
}
catch (err) {
geekgeekrunEditVersion = 0
}
if (geekgeekrunEditVersion < APP_GEEKGEEKRUN_EDIT_VERSION) {
isNeedExtractEditThisCookie = true
} else {
let manifest
try {
manifest = JSON.parse(fs.readFileSync(manifestFilePath, { encoding: 'utf-8' }))
if (!manifest.manifest_version || manifest.manifest_version <= 2) {
isNeedExtractEditThisCookie = true
}
}
catch {
console.log(`未能获取到文件内容`)
isNeedExtractEditThisCookie = true
}
}
if (isNeedExtractEditThisCookie) {

486
packages/pm/daemon.js Normal file
View File

@@ -0,0 +1,486 @@
// 如果是通过 Electron 运行,禁用 GUI 和 Dock 图标
if (typeof require !== 'undefined') {
try {
const { app } = require('electron');
if (app) {
// 隐藏 Dock 图标macOS
if (process.platform === 'darwin') {
app.dock?.hide();
}
// 防止显示窗口
app.on('ready', () => {
// 不创建任何窗口,保持后台运行
});
// 防止在没有窗口时退出
app.on('window-all-closed', (e) => {
e.preventDefault();
// 不退出应用,保持后台运行
});
}
} catch (e) {
// 不在 Electron 环境中,忽略
}
}
const net = require('net');
const { spawn } = require('child_process');
const path = require('path');
const split2 = require('split2');
const fs = require('fs')
const { tmpdir } = require('os')
const { randomUUID } = require('crypto')
const ipcWritePipe = fs.createWriteStream(null, { fd: 3 })
let ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
if (!ipcSocketName) {
process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}`
ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
}
const ipcSocketPath = process.platform === 'win32'
? `\\\\.\\pipe\\${ipcSocketName}`
: path.join(tmpdir(), `${ipcSocketName}.sock`)
const workers = new Map(); // workerId -> { process, status, restartCount, socket, latestScreenshot, latestScreenshotAt }
const userProcessClients = new Set(); // GUI客户端连接集合
const stoppedWorkers = new Set(); // 被用户主动停止的workerId集合用于防止竞态条件
const pidToProcessInfoMap = new Map()
const socketToWorkerIdSetMap = new WeakMap()
// 创建TCP服务器
const server = net.createServer((socket) => {
console.log('客户端已连接');
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON
// split2 会自动处理 TCP 分包问题,确保每条完整的消息(以换行符结尾)才会触发
const splitStream = split2();
socket.pipe(splitStream).on('data', (line) => {
const trimmedLine = line.toString().trim();
if (!trimmedLine) {
return; // 跳过空行
}
let _callbackUuid
try {
const message = JSON.parse(trimmedLine);
_callbackUuid = message._callbackUuid
handleMessage(socket, message);
} catch (parseError) {
console.error('解析JSON消息失败:', parseError.message);
console.error('原始数据:', trimmedLine.substring(0, 100)); // 只打印前100个字符
sendResponse(socket, _callbackUuid, { error: '无效的JSON格式', details: parseError.message });
}
});
splitStream.on('error', (err) => {
console.error('split2 流处理错误:', err);
sendResponse(socket, null, { error: '流处理失败' });
});
socket.on('error', (err) => {
console.error('Socket错误:', err);
});
socket.on('close', () => {
console.log('客户端已断开连接');
// 清理GUI客户端连接
userProcessClients.delete(socket);
// 清理工具进程连接
for (const [workerId, workerInfo] of workers.entries()) {
if (workerInfo.socket === socket) {
console.log(`工具进程 ${workerId} 的连接已断开`);
workerInfo.socket = null;
}
}
const workerIdSet = socketToWorkerIdSetMap.get(socket) || new Set()
;[...workerIdSet].forEach(workerId => {
stopWorker(workerId);
})
});
});
// 处理消息
function handleMessage(socket, message) {
console.log('收到消息:', message);
const _callbackUuid = message._callbackUuid
if (message.type === 'ping') {
sendResponse(socket, _callbackUuid, {
success: true,
message: 'pong'
});
return
}
if (message.type === 'user-process-register') {
// GUI客户端的控制消息
// 标记为GUI客户端
if (!userProcessClients.has(socket)) {
userProcessClients.add(socket);
}
sendResponse(socket, _callbackUuid, {
success: true,
});
return;
}
const workerId = message.workerId;
const workerInfo = workers.get(workerId);
switch (message.type) {
// 从工具进程发送的消息(数据、心跳、截图等)
case 'check-should-exit': {
const workerId = message.workerId;
const shouldExit = stoppedWorkers.has(workerId) || !workers.has(workerId);
sendResponse(socket, _callbackUuid, {
workerId: workerId,
shouldExit: shouldExit
});
if (shouldExit) {
console.log(`工具进程 ${workerId} 查询是否应该退出,返回: 是`);
}
return
}
case 'worker-to-gui-message': {
// 转发工具进程消息到GUI客户端
broadcastToGUI({
type: 'worker-to-gui-message',
workerId: workerId,
data: message.data || message,
timestamp: Date.now()
});
return
}
case 'worker-screenshot': {
if (workerInfo && message.data && message.data.screenshot /* && workerInfo.socket === socket */) {
// 如果携带截图信息,则在守护进程内缓存一份,供 get-status 使用
try {
workerInfo.latestScreenshot = message.data.screenshot;
workerInfo.latestScreenshotAt = Date.now();
} catch (e) {
console.error('缓存 worker 截图信息失败:', e);
}
}
return
}
// 从GUI进程发送的消息数据、心跳、截图等
case 'start-worker': {
const {
workerId,
command,
args,
env
} = message
if (workers.has(workerId)) {
console.log(`工具进程 ${workerId} 已在运行`);
return;
}
startWorker({
workerId,
command,
args,
env
});
sendResponse(socket, _callbackUuid, {
success: true,
message: `工具进程 ${message.workerId} 已启动`,
workerId: message.workerId
});
let socketToWorkerIdSet = socketToWorkerIdSetMap.get(socket)
if (
!(socketToWorkerIdSet instanceof Set)
) {
socketToWorkerIdSet = new Set()
socketToWorkerIdSetMap.set(
socket,
socketToWorkerIdSet
)
}
socketToWorkerIdSet.add(workerId)
return
}
case 'stop-worker': {
stopWorker(message.workerId);
sendResponse(socket, _callbackUuid, {
success: true,
message: `工具进程 ${message.workerId} 已停止`,
workerId: message.workerId
});
return
}
case 'get-status': {
const status = getWorkersStatus();
sendResponse(socket, _callbackUuid, {
success: true,
workers: status
});
return
}
default: {
sendResponse(socket, _callbackUuid, { error: '未知的消息类型' });
}
}
}
// 启动工具进程
function startWorker({ workerId, command, args, env }, restartCount = 0) {
const noAutoRestartExitCodeSet = new Set([0]);
(env.GEEKGEEKRUND_NO_AUTO_RESTART_EXIT_CODE ?? '')
.split(',')
.map(n => parseInt(n))
.forEach(n => noAutoRestartExitCodeSet.add(n))
console.log(`启动工具进程: ${workerId}${restartCount > 0 ? ` (重启第${restartCount}次)` : ''}`);
// 添加参数使工具进程在后台运行,不显示 UI
const workerProcess = spawn(command, args, {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
...env,
GEEKGEEKRUND_WORKER_ID: workerId,
GEEKGEEKRUND_RESTART_COUNT: restartCount.toString(),
}
});
let output = '';
workerProcess.stdout.on('data', (data) => {
output += data.toString();
console.log(`工具进程 ${workerId} 输出:`, data.toString().trim());
});
workerProcess.stderr.on('data', (data) => {
console.error(`工具进程 ${workerId} 错误:`, data.toString().trim());
});
workerProcess.on('exit', (code, signal) => {
console.log(`工具进程 ${workerId} 退出,代码: ${code}, 信号: ${signal}`);
const workerInfo = pidToProcessInfoMap.get(workerProcess.pid)
if (workerInfo) {
pidToProcessInfoMap.delete(workerProcess.pid)
// 关闭工具进程的TCP连接
if (workerInfo.socket) {
workerInfo.socket.destroy();
}
const shouldRestart = !noAutoRestartExitCodeSet.has(code) // && code !== null;
// 使用当前的 restartCount 加1而不是从 workerInfo 中取(因为可能已经被删除)
const restartCount = (workerInfo.restartCount || 0) + 1;
workers.delete(workerId);
// 通知GUI客户端工具进程已退出
broadcastToGUI({
type: 'worker-exited',
workerId: workerId,
code: code,
signal: signal,
restarting: shouldRestart && !stoppedWorkers.has(workerId),
restartCount: restartCount
});
// 如果进程意外退出(非正常停止),且不在停止列表中,自动重启
if (shouldRestart && !stoppedWorkers.has(workerId)) {
console.log(`工具进程 ${workerId} 意外退出,准备重启 (第${restartCount}次)`);
// 延迟重启,避免频繁重启
setTimeout(() => {
// 再次检查确保worker不在停止列表中且当前没有运行
if (!workers.has(workerId) && !stoppedWorkers.has(workerId)) {
startWorker({ workerId, command, args, env }, restartCount);
} else if (stoppedWorkers.has(workerId)) {
console.log(`工具进程 ${workerId} 在重启前被标记为停止,取消重启`);
// 从停止列表中移除,因为已经处理完毕
stoppedWorkers.delete(workerId);
broadcastToGUI({
type: 'worker-exited',
workerId: workerId,
code: code,
signal: signal,
restarting: false,
restartCount: restartCount
});
}
}, 2000);
} else if (stoppedWorkers.has(workerId)) {
// 如果是在停止列表中,清理标记
console.log(`工具进程 ${workerId} 已停止,清理停止标记`);
stoppedWorkers.delete(workerId);
}
} else {
// 如果workerInfo不存在可能是已经被stopWorker删除
// 检查停止列表,如果在则清理
if (stoppedWorkers.has(workerId)) {
stoppedWorkers.delete(workerId);
}
}
});
workerProcess.on('error', (err) => {
console.log(err)
})
const workerInfo = {
process: workerProcess,
status: 'running',
startTime: Date.now(),
restartCount, // 使用传入的重启次数
// socket: null, // 工具进程的TCP连接稍后由工具进程注册
// lastHeartbeat: null,
command,
args,
env,
workerId,
}
workers.set(workerId, workerInfo);
pidToProcessInfoMap.set(workerProcess.pid, workerInfo);
// 定期发送状态更新
broadcastStatus();
}
// 停止工具进程
function stopWorker(workerId) {
const workerInfo = workers.get(workerId);
// 无论workerInfo是否存在都添加到停止列表防止竞态条件
stoppedWorkers.add(workerId);
console.log(`停止工具进程: ${workerId} (已添加到停止列表)`);
if (!workerInfo) {
console.log(`工具进程 ${workerId} 不存在,但已标记为停止(防止重启)`);
// 通知GUI客户端
broadcastToGUI({
type: 'worker-disconnected',
workerId: workerId,
message: `工具进程 ${workerId} 已停止`
});
// 延迟发送状态更新
setTimeout(() => broadcastStatus(), 500);
return;
}
// 关闭工具进程的TCP连接
if (workerInfo.socket) {
workerInfo.socket.destroy();
}
workerInfo.process.kill('SIGTERM');
workers.delete(workerId);
// 通知GUI客户端
broadcastToGUI({
type: 'worker-disconnected',
workerId: workerId,
message: `工具进程 ${workerId} 已断开`
});
// 延迟发送状态更新
setTimeout(() => broadcastStatus(), 500);
}
// 获取所有工具进程状态
function getWorkersStatus() {
const status = [];
for (const [workerId, workerInfo] of workers.entries()) {
status.push({
workerId,
status: workerInfo.status,
uptime: Date.now() - workerInfo.startTime,
restartCount: workerInfo.restartCount || 0,
// connected: workerInfo.socket !== null && !workerInfo.socket.destroyed,
// lastHeartbeat: workerInfo.lastHeartbeat,
command: workerInfo.command,
args: workerInfo.args,
pid: workerInfo.process?.pid,
// 最新截图(通常是 data URL 或 base64 字符串),以及截图时间
screenshot: workerInfo.latestScreenshot ?? null,
screenshotAt: workerInfo.latestScreenshotAt ?? null,
});
}
return status;
}
// 广播状态更新给所有GUI客户端
function broadcastStatus() {
const status = getWorkersStatus();
broadcastToGUI({
type: 'status',
workers: status
});
}
// 广播消息给所有GUI客户端
function broadcastToGUI(message) {
userProcessClients.forEach(socket => {
if (!socket.destroyed) {
try {
sendResponse(socket, null, message);
} catch (e) {
console.error('广播消息失败:', e);
userProcessClients.delete(socket);
}
}
});
}
// 发送响应
function sendResponse(socket, _callbackUuid, response) {
try {
socket.write(JSON.stringify({
...response,
_callbackUuid
}) + '\n');
} catch (e) {
console.error('发送响应失败:', e);
}
}
// 启动服务器
new Promise((resolve, reject) => {
server.once('error', (err) => {
ipcWritePipe.write(
JSON.stringify({ type: 'DAEMON_FATAL', error: err }),
(err) => void err
)
reject(err)
})
server.listen(ipcSocketPath, (err) => {
console.log(`守护进程服务器运行在端口 ${ipcSocketPath}`);
ipcWritePipe.write(
JSON.stringify({ type: 'DAEMON_READY' }),
(err) => void err
)
if (!err) {
resolve(true)
}
else {
reject(err)
}
});
})
// 优雅关闭
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在关闭所有工具进程...');
for (const [workerId, workerInfo] of workers.entries()) {
workerInfo.process.kill('SIGTERM');
}
server.close(() => {
console.log('守护进程已关闭');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('收到SIGINT信号正在关闭所有工具进程...');
for (const [workerId, workerInfo] of workers.entries()) {
workerInfo.process.kill('SIGTERM');
}
server.close(() => {
console.log('守护进程已关闭');
process.exit(0);
});
});

25
packages/pm/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "@geekgeekrun/pm",
"version": "0.0.1",
"description": "Electron多进程架构",
"main": "main.js",
"scripts": {
"start:daemon": "electron ./daemon",
"dev:daemon": "electron ./daemon --dev",
"start:worker": "electron ./worker",
"dev:worker": "electron ./worker --dev"
},
"keywords": [
"electron",
"multi-process"
],
"author": "",
"license": "MIT",
"devDependencies": {
"electron": "28.2.0"
},
"dependencies": {
"JSONStream": "^1.3.5",
"split2": "^4.2.0"
}
}

View File

@@ -47,7 +47,8 @@ const main = async () => {
}
const hooks = {
daemonInitialized: new AsyncSeriesHook(),
puppeteerLaunched: new SyncHook(),
puppeteerLaunched: new SyncHook(['browser']),
pageGotten: new SyncHook(['page']),
pageLoaded: new SyncHook(),
cookieWillSet: new SyncHook(['cookies']),
userInfoResponse: new AsyncSeriesHook(['userInfo']),

View File

@@ -5,11 +5,13 @@
"main": "dist/index.js",
"dependencies": {
"cli-highlight": "^2.1.11",
"lodash": "^4.17.21",
"reflect-metadata": "^0.2.1",
"sqlite3": "5.1.6",
"typeorm": "0.3.11"
"better-sqlite3": "12.6.2",
"typeorm": "0.3.28"
},
"devDependencies": {
"@types/lodash": "^4.17.23",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm;
@Entity()
export class AutoStartChatRunRecord {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm;
@Entity()
export class BossActiveStatusRecord {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm;
@Entity()
export class BossInfo {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
@Entity()
export class BossInfoChangeLog {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
@Entity()
export class ChatMessageRecord {

View File

@@ -1,6 +1,6 @@
import { JobSource } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
export enum ChatStartupFrom {
AutoFromRecommendList = null,

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm
@Entity()
export class CompanyInfo {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, PrimaryGeneratedColumn, Column } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, PrimaryGeneratedColumn, Column } = typeorm
@Entity()
export class CompanyInfoChangeLog {

View File

@@ -1,6 +1,6 @@
import { JobHireStatus } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm;
@Entity()
export class JobHireStatusRecord {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm
@Entity()
export class JobInfo {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, PrimaryGeneratedColumn, Column } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, PrimaryGeneratedColumn, Column } = typeorm
@Entity()
export class JobInfoChangeLog {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, PrimaryGeneratedColumn, Column, Index } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, PrimaryGeneratedColumn, Column, Index } = typeorm
@Entity()
@Index(["providerCompleteApiUrl", "model", "providerApiSecret"])

View File

@@ -1,7 +1,7 @@
import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
import * as typeorm from 'typeorm';
import { ChatStartupFrom } from "./ChatStartupLog";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
@Entity()
export class MarkAsNotSuitLog {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm
@Entity()
export class UserInfo {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { ViewEntity, ViewColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
boss_info.encryptBossId,

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { ViewEntity, ViewColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
job_info.*,

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { ViewEntity, ViewColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
company_info.*

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { ViewEntity, ViewColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
job_info.*,

View File

@@ -1,7 +1,7 @@
import { requireTypeorm } from "../utils/module-loader";
import * as typeorm from 'typeorm';
import { ChatStartupFrom } from "./ChatStartupLog";
import { MarkAsNotSuitReason } from "../enums";
const { ViewEntity, ViewColumn } = requireTypeorm();
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
job_info.*,

View File

@@ -6,6 +6,7 @@ export enum MarkAsNotSuitReason {
JOB_CITY_NOT_SUIT = 4,
JOB_WORK_EXP_NOT_SUIT = 5,
JOB_SALARY_NOT_SUIT = 6,
COMPANY_NAME_NOT_SUIT = 7,
}
export enum MarkAsNotSuitOp {

View File

@@ -1,6 +1,5 @@
import "reflect-metadata";
import { type DataSource } from "typeorm";
import { requireTypeorm } from "./utils/module-loader";
import { BossInfo } from "./entity/BossInfo";
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
@@ -22,7 +21,6 @@ import { ChatMessageRecord } from './entity/ChatMessageRecord'
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
import { JobHireStatusRecord } from './entity/JobHireStatusRecord'
import sqlite3 from 'sqlite3';
import {
saveChatStartupRecord,
saveJobInfoFromRecommendPage,
@@ -40,17 +38,17 @@ import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/17460923
import { Init1000000000000 } from "./migrations/1000000000000-Init";
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
import { AddJobHireStatusTable1766466476822 } from "./migrations/1766466476822-AddJobHireStatusTable";
const lodashImportPromise = import('lodash-es')
import chunk from 'lodash/chunk'
import * as typeorm from 'typeorm'
export function initDb(dbFilePath) {
const { DataSource } = requireTypeorm()
const { DataSource } = typeorm
const appDataSource = new DataSource({
type: "sqlite",
type: "better-sqlite3",
synchronize: false,
logging: true,
logger: "simple-console",
database: dbFilePath,
driver: sqlite3, // The important line
entities: [
ChatStartupLog,
BossInfo,
@@ -97,6 +95,25 @@ export default class SqlitePlugin {
userInfo = null
apply(hooks) {
hooks.pageGotten.tap(
'SqlitePlugin',
(page) => {
page.on('response', async (response) => {
const ds = await this.initPromise;
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
const data = await response.json()
if (data.code === 0) {
await saveJobInfoFromRecommendPage(await ds, data.zpData)
await saveJobHireStatusRecord(await ds, {
encryptJobId: data.zpData.jobInfo.encryptId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
}
})
}
)
hooks.userInfoResponse.tapPromise(
"SqlitePlugin",
async (userInfoResponse) => {
@@ -183,7 +200,6 @@ export default class SqlitePlugin {
if (chattedJobIds.length === 0) {
return
}
const { chunk } = await lodashImportPromise
const chattedJobIdChunks = chunk(chattedJobIds, 200)
const chattedBossIds = [];
for (const chattedJobIdChunk of chattedJobIdChunks) {

View File

@@ -1,4 +0,0 @@
export function requireTypeorm () {
const importResult = require('typeorm')
return importResult
}

View File

@@ -18,8 +18,6 @@ export default defineConfig({
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'

View File

@@ -1,7 +1,7 @@
{
"name": "geekgeekrun-ui",
"version": "0.14.2",
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
"description": "BOSS 炸弹 - 自动开聊BOSS,助力每位打工人求职!",
"main": "./out/main/index.js",
"author": "geekgeekrun",
"productName": "GeekGeekRun",
@@ -9,10 +9,6 @@
"scripts": {
"start": "node scripts/run-build-sqlite-plugin.mjs && electron-vite preview",
"dev": "node scripts/run-build-sqlite-plugin.mjs && electron-vite dev",
"dev:geek-auto-start-chat-with-boss-main-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=geekAutoStartWithBossMain pnpm run dev",
"dev:geek-auto-start-chat-with-boss-daemon-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=geekAutoStartWithBossDaemon pnpm run dev",
"dev:check-and-download-dependencies-for-init-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=checkAndDownloadDependenciesForInit pnpm run dev",
"dev:launch-bosszhipin-login-page-with-preload-extension-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=launchBossZhipinLoginPageWithPreloadExtension pnpm run dev",
"build": "node scripts/run-build-sqlite-plugin.mjs && electron-vite build",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
@@ -30,16 +26,15 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@geekgeekrun/dingtalk-plugin": "workspace:*",
"@geekgeekrun/geek-auto-start-chat-with-boss": "workspace:*",
"@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension": "workspace:*",
"@geekgeekrun/pm": "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",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"mitt": "^3.0.1",
"node-machine-id": "^1.1.12",
@@ -53,10 +48,13 @@
"@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@geekgeekrun/dingtalk-plugin": "workspace:*",
"@geekgeekrun/geek-auto-start-chat-with-boss": "workspace:*",
"@inquirer/prompts": "^7.0.1",
"@inquirer/select": "^4.0.1",
"@puppeteer/browsers": "^2.11.0",
"@rushstack/eslint-patch": "^1.7.1",
"@types/lodash": "^4.17.23",
"@types/node": "^18.19.9",
"@unocss/preset-attributify": "^0.58.5",
"@unocss/preset-icons": "^0.58.5",
@@ -66,13 +64,13 @@
"@vue/eslint-config-typescript": "^12.0.0",
"animate.css": "^4.1.1",
"cross-env": "^7.0.3",
"electron": "28.2.0",
"electron": "39.2.7",
"electron-builder": "^24.9.1",
"electron-vite": "^2.0.0",
"element-plus": "^2.8.4",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.1",
"find-chrome-bin": "^2.0.4",
"find-chrome-bin": "2.0.4",
"js-yaml": "^4.1.0",
"normalize.css": "^8.0.1",
"prettier": "^3.2.4",
@@ -89,4 +87,4 @@
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.27"
}
}
}

View File

@@ -1 +1,2 @@
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'

View File

@@ -5,8 +5,7 @@ export enum AUTO_CHAT_ERROR_EXIT_CODE {
ERR_INTERNET_DISCONNECTED = 83,
ACCESS_IS_DENIED = 84,
PUPPETEER_IS_NOT_EXECUTABLE = 85,
AUTO_START_CHAT_DAEMON_PROCESS_SUICIDE = 86,
AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87,
LLM_UNAVAILABLE = 86,
}
export enum RECHAT_CONTENT_SOURCE {
@@ -18,3 +17,9 @@ export enum RECHAT_LLM_FALLBACK {
SEND_LOOK_FORWARD_EMOTION = 1,
EXIT_REMINDER_PROGRAM = 2
}
export enum RUNNING_STATUS_ENUM {
RUNNING = 0,
NORMAL_EXITED = 1,
ERROR_EXITED = 2
}

View File

@@ -0,0 +1,18 @@
export const getAutoStartChatSteps = () => [
{
id: 'worker-launch',
describe: '启动子进程'
},
{
id: 'puppeteer-executable-check',
describe: 'Puppeteer 可执行程序检查'
},
{
id: 'basic-cookie-check',
describe: 'Cookie 格式检查'
},
{
id: 'login-status-check',
describe: '登录状态检查'
}
]

View File

@@ -0,0 +1,4 @@
import path from 'node:path'
import os from 'node:os'
export const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')

View File

@@ -0,0 +1,26 @@
import { ipcMain } from 'electron'
import {
createBrowserAssistantWindow,
browserAssistantWindow
} from '../window/browserAssistantWindow'
export async function configWithBrowserAssistant({ windowOption, autoFind } = {}) {
return new Promise((resolve, reject) => {
createBrowserAssistantWindow({ ...windowOption }, { autoFind })
let processDone = false
function handler() {
processDone = true
browserAssistantWindow.close()
}
ipcMain.once('browser-config-saved', handler)
browserAssistantWindow.once('closed', () => {
ipcMain.off('browser-config-saved', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED_CONFIG_BROWSER'))
}
})
})
}

View File

@@ -0,0 +1,93 @@
import { sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
import minimist from 'minimist'
import { loginWithCookieAssistant } from './login-with-cookie-assistant'
import { checkCookieListFormat } from '../../common/utils/cookie'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { readStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
export class CookieInvalidHandlePlugin {
apply(hooks) {
hooks.cookieWillSet.tapPromise('CookieInvalidHandlePlugin', async (cookies) => {
let isValid = checkCookieListFormat(cookies)
while (!isValid) {
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
await sleep(2000)
const newCookies = readStorageFile('boss-cookies.json')
isValid = checkCookieListFormat(newCookies)
if (isValid) {
cookies.length = 0
for (const cookie of newCookies) {
cookies.push(cookie)
}
}
} catch (e) {
if (e?.message === 'USER_CANCELLED_LOGIN') {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'fulfilled'
},
runRecordId
}
})
})
hooks.userInfoResponse.tapPromise('CookieInvalidHandlePlugin', async (userInfoResponse) => {
if (userInfoResponse.code === 0) {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'fulfilled'
},
runRecordId
}
})
return
}
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
} catch (e) {
if (e?.message === 'USER_CANCELLED_LOGIN') {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
// throw new Error('THROW_FOR_RETRY')
return Promise.reject(new Error('THROW_FOR_RETRY'))
})
}
}

View File

@@ -3,6 +3,11 @@ import os from 'os'
import path from 'path'
import buildInfo from '../../common/build-info.json'
import { ensureStorageFileExist } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import {
createFirstLaunchNoticeWindow,
firstLaunchNoticeWindow
} from '../window/firstLaunchNoticeWindow'
import { ipcMain } from 'electron'
export const firstLaunchNoticeApproveFlagPath = path.join(
os.homedir(),
@@ -10,8 +15,28 @@ export const firstLaunchNoticeApproveFlagPath = path.join(
'ui-first-launch-notice-flag'
)
export const isFirstLaunchNoticeApproveFlagExist = () => fs.existsSync(firstLaunchNoticeApproveFlagPath)
export const isFirstLaunchNoticeApproveFlagExist = () =>
fs.existsSync(firstLaunchNoticeApproveFlagPath)
export const createFirstLaunchNoticeApproveFlag = () => {
ensureStorageFileExist()
fs.writeFileSync(firstLaunchNoticeApproveFlagPath, buildInfo.version)
}
export async function waitForUserApproveAgreement({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createFirstLaunchNoticeWindow({ ...windowOption })
let processDone = false
function handler() {
processDone = true
firstLaunchNoticeWindow.close()
}
ipcMain.once('first-launch-notice-approve', handler)
firstLaunchNoticeWindow.once('closed', () => {
ipcMain.off('first-launch-notice-approve', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED'))
}
})
})
}

View File

@@ -0,0 +1,23 @@
import { ipcMain } from 'electron'
import { createCookieAssistantWindow, cookieAssistantWindow } from '../window/cookieAssistantWindow';
export async function loginWithCookieAssistant({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createCookieAssistantWindow({ ...windowOption })
let processDone = false
function handler() {
processDone = true
cookieAssistantWindow.close()
}
ipcMain.once('cookie-saved', handler)
cookieAssistantWindow.once('closed', () => {
ipcMain.off('cookie-saved', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED_LOGIN'))
}
})
})
}

View File

@@ -0,0 +1,28 @@
import { ipcMain } from 'electron'
import {
createBrowserDownloadProgressWindow,
browserDownloadProgressWindow
} from '../window/browserDownloadProgressWindow'
export async function openBrowserDownloadWindow({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createBrowserDownloadProgressWindow({ ...windowOption })
let processDone = false
let pathOfDownloadedBrowser = null
function handler(_, executablePath) {
pathOfDownloadedBrowser = executablePath
processDone = true
browserDownloadProgressWindow.close()
}
ipcMain.once('browser-download-done', handler)
browserDownloadProgressWindow.once('closed', () => {
ipcMain.off('browser-download-done', handler)
if (processDone) {
resolve(pathOfDownloadedBrowser)
} else {
reject(new Error('USER_CANCELLED_CONFIG_BROWSER'))
}
})
})
}

View File

@@ -0,0 +1,74 @@
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat'
import { daemonEE, sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
import { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db'
import minimist from 'minimist'
export async function runCommon({ mode }) {
await sendToDaemon(
{
type: 'user-process-register'
},
{
needCallback: true
}
)
const taskList = (
await sendToDaemon(
{
type: 'get-status'
},
{
needCallback: true
}
)
)?.workers
const runningTask = taskList?.find((it) => it.workerId === mode)
if (runningTask) {
const commandlineArgs = minimist(runningTask.args ?? [])
const runRecordId = Number(commandlineArgs['run-record-id'])
console.log('任务已在运行中')
return {
runRecordId,
isAlreadyRunning: true
}
}
const currentRunRecord = (await saveAndGetCurrentRunRecord())?.data
const subProcessEnv = {
...process.env,
GEEKGEEKRUND_NO_AUTO_RESTART_EXIT_CODE: [
AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE,
AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID,
AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE
].join(',')
}
const args =
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
: [`--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
await sendToDaemon(
{
type: 'start-worker',
workerId: mode,
command: process.argv[0],
args,
env: subProcessEnv
},
{
needCallback: true
}
)
daemonEE.on('message', (message) => {
if (message.type === 'worker-exited') {
if (
message.workerId === mode &&
!message.restarting &&
globalThis.GEEKGEEKRUN_PROCESS_ROLE !== 'ui'
) {
process.exit(0)
}
}
})
return {
runRecordId: currentRunRecord?.id
}
}

View File

@@ -9,7 +9,7 @@ export enum DOWNLOAD_ERROR_EXIT_CODE {
DOWNLOAD_ERROR = 80
}
export const checkAndDownloadDependenciesForInit = async () => {
export const downloadDependenciesForInit = async () => {
process.on('disconnect', () => app.exit())
process.on('uncaughtException', () => app.exit(DOWNLOAD_ERROR_EXIT_CODE.DOWNLOAD_ERROR))
app.dock?.hide()
@@ -50,7 +50,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
timeoutTimer = setTimeout(() => {
// will encounter this when network disconnected when downloading
promiseWithResolver.reject(new Error('PROGRESS_NOT_CHANGED_TOO_LONG'))
}, 5 * 1000)
}, 10 * 1000)
} else {
clearTimeout(throttleProgressTimer)
throttleProgressTimer = null
@@ -73,7 +73,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
)
throttleProgressTimer = setTimeout(() => {
throttleProgressTimer = null
}, 2500)
}, 500)
}
}
})
@@ -94,11 +94,13 @@ export const checkAndDownloadDependenciesForInit = async () => {
pipe,
JSON.stringify({
type: 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR',
...err instanceof Error ? {
name: err.name,
message: err.message,
stack: err.stack
} : null
...(err instanceof Error
? {
name: err.name,
message: err.message,
stack: err.stack
}
: null)
}) + '\r\n'
)
await sleep(1000)

View File

@@ -40,13 +40,6 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
await removeLastUsedAndAvailableBrowserPath()
return null
}
// blacklist browser
if (path.includes(`Microsoft\\Edge\\Application\\msedge.exe`)) {
await removeLastUsedAndAvailableBrowserPath()
return null
}
return {
executablePath: path,
browser

View File

@@ -1,5 +1,3 @@
import * as path from 'node:path'
import * as os from 'node:os'
import * as fs from 'node:fs'
import type { InstalledBrowser } from '@puppeteer/browsers'
import {
@@ -9,6 +7,8 @@ import {
removeLastUsedAndAvailableBrowserPath
} from '../browser-history'
import gtag from '../../../../utils/gtag'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../../common/constant'
import { cacheDir } from '../../../../constant'
const getPuppeteerManagerModule = async () => {
const puppeteerManager = await import('@puppeteer/browsers')
@@ -16,8 +16,6 @@ const getPuppeteerManagerModule = async () => {
return puppeteerManager
}
const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')
const getExpectCachedPuppeteerExecutable = async (): Promise<BrowserInfo> => {
const puppeteerManager = await getPuppeteerManagerModule()
@@ -97,29 +95,36 @@ export const checkAndDownloadPuppeteerExecutable = async (
return installedBrowser
}
export const getAnyAvailablePuppeteerExecutable = async (): Promise<BrowserInfo | null> => {
const lastUsedOne = await getLastUsedAndAvailableBrowser()
if (lastUsedOne) {
return lastUsedOne
export const getAnyAvailablePuppeteerExecutable = async ({
ignoreCached = false,
noSave = false
}: {
ignoreCached?: boolean
noSave?: boolean
} = {}): Promise<BrowserInfo | null> => {
if (!ignoreCached) {
const lastUsedOne = await getLastUsedAndAvailableBrowser()
if (lastUsedOne) {
return lastUsedOne
}
}
// find existed browser - the fallback one
if (await checkCachedPuppeteerExecutable()) {
const cachedOne = await getExpectCachedPuppeteerExecutable()
!noSave && (await saveLastUsedAndAvailableBrowserInfo(cachedOne))
return cachedOne
}
// find existed browser - the one maybe actively installed by user or ship with os like Edge on windows
try {
const existedOne = await findAndLocateUserInstalledChromiumExecutableSync()
await saveLastUsedAndAvailableBrowserInfo(existedOne)
!noSave && (await saveLastUsedAndAvailableBrowserInfo(existedOne))
// save its path
return existedOne
} catch (err) {
console.error(err)
console.log('no existed browser path found')
}
// find existed browser - the fallback one
if (await checkCachedPuppeteerExecutable()) {
const cachedOne = await getExpectCachedPuppeteerExecutable()
await saveLastUsedAndAvailableBrowserInfo(cachedOne)
return cachedOne
}
// if no one available, then return null and remove last used browser
await removeLastUsedAndAvailableBrowserPath()
return null

View File

@@ -1,129 +0,0 @@
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import childProcess from 'node:child_process'
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
import { app, dialog } from 'electron'
import fs, { WriteStream } from 'node:fs'
import { pipeWriteRegardlessError } from '../utils/pipe'
import * as JSONStream from 'JSONStream'
import { initPowerSaveBlocker } from './power-saver-blocker'
import gtag from '../../utils/gtag'
import { initDb } from '@geekgeekrun/sqlite-plugin'
import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { AutoStartChatRunRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/AutoStartChatRunRecord'
import minimist from 'minimist'
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
const isUiDev = process.env.NODE_ENV === 'development'
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
v = 3000
}
return v
})()
function runWithDaemon({ runRecordId, runMode, parentProcessPipe }) {
const subProcessOfCore = childProcess.spawn(
process.argv[0],
isUiDev
? [process.argv[1], `--run-record-id=${runRecordId}`, `--mode=${runMode}`]
: [`--run-record-id=${runRecordId}`, `--mode=${runMode}`],
{
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
}
)
subProcessOfCore!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => {
const data = raw
switch (data.type) {
case 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED': {
pipeWriteRegardlessError(
parentProcessPipe as WriteStream,
JSON.stringify({
type: data.type
})
)
break
}
default: {
return
}
}
})
subProcessOfCore.once('exit', async (exitCode: number) => {
if (
[...Object.values(AUTO_CHAT_ERROR_EXIT_CODE)]
.filter((it) => typeof it === 'number')
.includes(exitCode)
) {
console.log(
`[Run core daemon] Child process exit with reason ${AUTO_CHAT_ERROR_EXIT_CODE[exitCode]}.`
)
process.exit(exitCode)
return
}
console.log(
`[Run core daemon] Child process exit with code ${exitCode}, an internal error may not be caught, and will be restarted in ${rerunInterval}ms.`
)
await sleep(rerunInterval)
runWithDaemon({ runRecordId, runMode, parentProcessPipe })
})
}
export async function runAutoChatWithDaemon() {
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
if (!['geekAutoStartWithBossMain'].includes(commandlineArgs['mode-to-daemon'])) {
await new Promise((resolve) => {
app.once('ready', () => resolve(undefined))
})
dialog.showMessageBoxSync({
type: 'error',
message: `守护进程不支持 ${commandlineArgs['mode-to-daemon'] ?? '(默认)'} 模式`
})
app.exit()
return
}
app.dock?.hide()
process.on('disconnect', () => {
app.exit()
})
let pipe: null | fs.WriteStream = null
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.error('pipe is not available')
app.exit(1)
}
const disposePowerSaveBlocker = initPowerSaveBlocker()
app.once('quit', disposePowerSaveBlocker)
process.on('SIGINT', () => {
process.exit()
})
const ds = await initDb(getPublicDbFilePath())
const autoStartChatRunRecord = new AutoStartChatRunRecord()
autoStartChatRunRecord.date = new Date()
const autoStartChatRunRecordRepository = ds.getRepository(AutoStartChatRunRecord)
const result = await autoStartChatRunRecordRepository.save(autoStartChatRunRecord)
runWithDaemon({
runRecordId: result.id,
runMode: commandlineArgs['mode-to-daemon'],
parentProcessPipe: pipe
})
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'AUTO_START_CHAT_DAEMON_PROCESS_STARTUP'
})
)
gtag('daemon_ready', { mode: commandlineArgs['mode-to-daemon'] ?? '' })
}
attachListenerForKillSelfOnParentExited()

View File

@@ -1,10 +0,0 @@
import { powerSaveBlocker } from 'electron'
export const initPowerSaveBlocker = (
type: 'prevent-app-suspension' | 'prevent-display-sleep' = 'prevent-app-suspension'
) => {
const id = powerSaveBlocker.start(type)
return function disposePowerSaveBlocker() {
return powerSaveBlocker.stop(id)
}
}

View File

@@ -5,19 +5,28 @@ import {
readConfigFile,
getPublicDbFilePath
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import * as fs from 'fs'
import { pipeWriteRegardlessError } from '../utils/pipe'
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
// import { pipeWriteRegardlessError } from '../utils/pipe'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
import minimist from 'minimist'
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
import gtag from '../../utils/gtag'
import GtagPlugin from '../../utils/gtag/GtagPlugin'
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
// import { PeriodPushCurrentPageScreenshotPlugin } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
import { CookieInvalidHandlePlugin } from '../../features/cookie-invalid-handle-plugin'
import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
const { default: SqlitePlugin } = SqlitePluginModule
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在退出')
process.exit(0)
})
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
@@ -33,14 +42,54 @@ const initPlugins = (hooks) => {
new DingtalkPlugin(dingTalkAccessToken).apply(hooks)
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
new GtagPlugin().apply(hooks)
// new PeriodPushCurrentPageScreenshotPlugin().apply(hooks)
new CookieInvalidHandlePlugin().apply(hooks)
}
let isParentProcessDisconnect = false
process.once('disconnect', () => {
isParentProcessDisconnect = true
})
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
const runAutoChat = async () => {
app.dock?.hide()
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'rejected'
},
runRecordId
}
})
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
return
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'fulfilled'
},
runRecordId
}
})
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
const { initPuppeteer, mainLoop, closeBrowserWindow, autoStartChatEventBus } = await import(
'@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
)
@@ -48,43 +97,13 @@ const runAutoChat = async () => {
closeBrowserWindow()
app.exit()
})
app.dock?.hide()
let pipe: null | fs.WriteStream = null
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.warn('pipe is not available')
}
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'INITIALIZE_PUPPETEER'
}) + '\r\n'
)
try {
await initPuppeteer()
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
}) + '\r\n'
)
} catch (err) {
console.error(err)
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
return
}
const isPuppeteerExecutable = !!(await getAnyAvailablePuppeteerExecutable())
if (!isPuppeteerExecutable) {
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
return
}
await initPuppeteer()
const hooks = {
puppeteerLaunched: new SyncHook(),
puppeteerLaunched: new SyncHook(['browser']),
pageGotten: new SyncHook(['page']),
pageLoaded: new SyncHook(),
cookieWillSet: new SyncHook(['cookies']),
cookieWillSet: new AsyncSeriesHook(['cookies']),
userInfoResponse: new AsyncSeriesHook(['userInfo']),
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']),
@@ -101,23 +120,11 @@ const runAutoChat = async () => {
initPlugins(hooks)
gtag('run_auto_chat_with_boss_main_ready')
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED' //geek-auto-start-chat-with-boss-started
}) + '\r\n'
)
autoStartChatEventBus.once('LOGIN_STATUS_INVALID', () => {
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started
}) + '\r\n'
)
})
while (![isParentProcessDisconnect].includes(true)) {
while (true) {
try {
await mainLoop(hooks)
} catch (err) {
@@ -126,7 +133,7 @@ const runAutoChat = async () => {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录Boss直聘`
detail: `请重新登录BOSS直聘`
})
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
break
@@ -139,9 +146,18 @@ const runAutoChat = async () => {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED)
break
}
if (err.message.includes(`Could not find Chrome`) || err.message.includes(`no executable was found`)) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
break
}
}
closeBrowserWindow?.()
console.error(err)
const shouldExit = await checkShouldExit()
if (shouldExit) {
app.exit()
return
}
console.log(
`[Run core main] An internal error is caught, and browser will be restarted in ${rerunInterval}ms.`
)
@@ -150,14 +166,32 @@ const runAutoChat = async () => {
}
}
export const waitForProcessHandShakeAndRunAutoChat = () => {
let pipe: null | fs.WriteStream = null
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.error('pipe is not available')
app.exit(1)
}
export const waitForProcessHandShakeAndRunAutoChat = async () => {
await app.whenReady()
app.on('window-all-closed', (e) => {
e.preventDefault()
})
initPublicIpc()
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'worker-launch',
status: 'fulfilled'
},
runRecordId
}
})
runAutoChat()
}

View File

@@ -1,7 +1,12 @@
import { app } from 'electron'
import { main, loginEventBus } from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
import { app, dialog } from 'electron'
import {
main,
loginEventBus
} from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
import { pipeWriteRegardlessError } from './utils/pipe'
import fs from "node:fs";
import fs from 'node:fs'
import { getLastUsedAndAvailableBrowser } from './DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../features/config-with-browser-assistant'
export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
process.on('disconnect', () => app.exit())
@@ -18,15 +23,26 @@ export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
type: 'INITIALIZE_PUPPETEER'
}) + '\r\n'
)
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
app.exit(1)
}
const { initPuppeteer } = await import('@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs')
try {
await initPuppeteer()
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
}) + '\r\n'
)
} catch (err) {
console.error(err)
app.exit(1)

View File

@@ -0,0 +1,5 @@
;(async () => {
await import('@geekgeekrun/pm/daemon.js')
})()
export {}

View File

@@ -0,0 +1,145 @@
import { randomUUID } from 'node:crypto'
import { EventEmitter } from 'node:events'
import { tmpdir } from 'node:os'
import path from 'node:path'
import net from 'node:net'
import split2 from 'split2'
let daemonClient = null
export const daemonEE = new EventEmitter()
const waitForCallbackTaskMap = new Map()
export function getSocketPath(socketName) {
const ipcSocketPath =
process.platform === 'win32'
? `\\\\.\\pipe\\${socketName}`
: path.join(tmpdir(), `${socketName}.sock`)
return ipcSocketPath
}
// 连接到守护进程
export async function connectToDaemon() {
daemonClient = new net.Socket()
let isConnected = false
await new Promise((resolve, reject) => {
const ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
const ipcSocketPath = getSocketPath(ipcSocketName)
daemonClient.connect(ipcSocketPath, 'localhost', () => {
isConnected = true
console.log('已连接到守护进程')
daemonEE.emit('connect')
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON
const splitStream = split2()
daemonClient.pipe(splitStream).on('data', (line) => {
const trimmedLine = line.toString().trim()
if (!trimmedLine) {
return // 跳过空行
}
try {
const message = JSON.parse(trimmedLine)
daemonEE.emit('message', message)
// FIXME:
// console.log('收到守护进程消息:', message)
if (message._callbackUuid) {
const callbackInfo = waitForCallbackTaskMap.get(message._callbackUuid)
if (callbackInfo) {
const isError = message._isError
if (isError) {
callbackInfo.reject(message)
} else {
callbackInfo.resolve(message)
}
waitForCallbackTaskMap.delete(message._callbackUuid)
}
}
// 转发消息到渲染进程
// if (mainWindow) {
// mainWindow.webContents.send('daemon-message', message);
// }
} catch (parseError) {
console.error('解析守护进程消息失败:', parseError.message)
console.error('原始数据:', trimmedLine.substring(0, 100))
}
})
splitStream.on('error', (err) => {
console.error('split2 流处理错误:', err)
})
daemonClient.on('close', () => {
if (!isConnected) {
return
}
console.log('守护进程连接已关闭')
daemonEE.emit('close')
})
resolve(true)
// 通知渲染进程连接成功
// if (mainWindow) {
// mainWindow.webContents.send('daemon-connected');
// }
})
daemonClient.on('close', () => {
if (isConnected) {
return
}
reject(new Error('连接到守护进程超时'))
})
daemonClient.on('error', (err) => {
console.error('守护进程连接错误:', err)
// daemonEE.emit('error', err)
reject(err)
})
})
}
// 向守护进程发送消息
export function sendToDaemon(message, { needCallback = false, timeout = undefined } = {}) {
const _callbackUuid = randomUUID()
if (daemonClient && !daemonClient.destroyed) {
daemonClient.write(
JSON.stringify({
...message,
_callbackUuid
}) + '\n'
)
if (needCallback) {
let resolve, reject
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})
waitForCallbackTaskMap.set(_callbackUuid, { resolve, reject })
promise.finally(() => waitForCallbackTaskMap.delete(_callbackUuid))
let timeoutTimer
if (!isNaN(parseInt(timeout))) {
timeoutTimer = setTimeout(() => {
reject(new Error(`Callback timeout after ${timeout}ms`))
}, timeout)
}
promise.finally(() => {
clearTimeout(timeoutTimer)
})
return promise
}
} else {
console.error('守护进程未连接')
}
return undefined
}
// // IPC处理从渲染进程接收消息并转发到守护进程
// ipcMain.on('send-to-daemon', (event, message) => {
// sendToDaemon(message);
// });
// // IPC处理启动工具进程
// ipcMain.on('start-worker', (event, { workerId, command, args, env }) => {
// sendToDaemon({ type: 'start-worker', workerId, command, args, env });
// });
export function closeDaemonClient() {
daemonClient?.destroy()
}

View File

@@ -5,6 +5,8 @@ import './app-menu'
import initIpc from './ipc'
import gtag from '../../utils/gtag'
import initPublicIpc from '../../utils/initPublicIpc'
import { sendToDaemon, closeDaemonClient } from './connect-to-daemon'
export function openSettingWindow() {
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
if (!app.requestSingleInstanceLock()) {
@@ -65,4 +67,25 @@ export function openSettingWindow() {
globalShortcut.unregister('Command+Option+Shift+/')
})
})
whenReadyPromise.then(async () => {
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
await sendToDaemon(
{
type: 'user-process-register'
},
{
needCallback: true
}
)
})
app.on('window-all-closed', closeDaemonClient)
app.on('before-quit', closeDaemonClient)
}

View File

@@ -1,22 +1,18 @@
import { ipcMain, shell, app } from 'electron'
import { ipcMain, shell, app, dialog, BrowserWindow } from 'electron'
import path from 'path'
import * as childProcess from 'node:child_process'
import {
ensureConfigFileExist,
ensureStorageFileExist,
configFileNameList,
readConfigFile,
writeConfigFile,
readStorageFile,
writeStorageFile,
storageFilePath
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { ChildProcess } from 'child_process'
import * as JSONStream from 'JSONStream'
import { checkCookieListFormat } from '../../../../common/utils/cookie'
import { getAnyAvailablePuppeteerExecutable } from '../../../flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../../common/enums/auto-start-chat'
import { getAnyAvailablePuppeteerExecutable } from '../../DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index'
import { mainWindow } from '../../../window/mainWindow'
import {
getAutoStartChatRecord,
@@ -36,11 +32,11 @@ import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/re
import {
getValidTemplate,
requestNewMessageContent
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
autoReminderPromptTemplateFileName,
writeDefaultAutoRemindPrompt
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
checkIsResumeContentValid,
resumeContentEnoughDetect
@@ -52,6 +48,16 @@ import {
import { RequestSceneEnum } from '../../../features/llm-request-log'
import { checkUpdateForUi } from '../../../features/updater'
import gtag from '../../../utils/gtag'
import { daemonEE, sendToDaemon } from '../connect-to-daemon'
import { runCommon } from '../../../features/run-common'
import { loginWithCookieAssistant } from '../../../features/login-with-cookie-assistant'
import { configWithBrowserAssistant } from '../../../features/config-with-browser-assistant'
import {
createFirstLaunchNoticeApproveFlag,
isFirstLaunchNoticeApproveFlagExist,
waitForUserApproveAgreement
} from '../../../features/first-launch-notice-window'
import { getLastUsedAndAvailableBrowser } from '../../DOWNLOAD_DEPENDENCIES/utils/browser-history'
export default function initIpc() {
ipcMain.handle('fetch-config-file-content', async () => {
@@ -175,6 +181,12 @@ export default function initIpc() {
if (hasOwn(payload, 'sageTimePauseMinute')) {
bossConfig.sageTimePauseMinute = payload.sageTimePauseMinute
}
if (hasOwn(payload, 'blockCompanyNameRegExpStr')) {
bossConfig.blockCompanyNameRegExpStr = payload.blockCompanyNameRegExpStr
}
if (hasOwn(payload, 'blockCompanyNameRegMatchStrategy')) {
bossConfig.blockCompanyNameRegMatchStrategy = payload.blockCompanyNameRegMatchStrategy
}
promiseArr.push(writeConfigFile('boss.json', bossConfig))
@@ -187,262 +199,111 @@ export default function initIpc() {
return await Promise.all(promiseArr)
})
ipcMain.handle('read-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await readStorageFile(payload.fileName)
})
ipcMain.handle('write-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
})
// const currentExecutablePath = app.getPath('exe')
// console.log(currentExecutablePath)
ipcMain.handle('prepare-run-geek-auto-start-chat-with-boss', async () => {
mainWindow?.webContents.send('locating-puppeteer-executable')
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
}
mainWindow?.webContents.send('puppeteer-executable-is-located')
})
let subProcessOfPuppeteer: ChildProcess | null = null
ipcMain.handle('run-geek-auto-start-chat-with-boss', async () => {
if (subProcessOfPuppeteer) {
return
}
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
}
const subProcessEnv = {
...process.env,
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
}
subProcessOfPuppeteer = childProcess.spawn(
process.argv[0],
[
process.argv[1],
`--mode=geekAutoStartWithBossDaemon`,
`--mode-to-daemon=geekAutoStartWithBossMain`
],
{
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => {
const mode = 'geekAutoStartWithBossMain'
const { runRecordId } = await runCommon({ mode })
daemonEE.on('message', function handler(message) {
if (message.workerId !== mode) {
return
}
if (message.type === 'worker-exited') {
mainWindow?.webContents.send('worker-exited', message)
}
)
// console.log(subProcessOfPuppeteer)
return new Promise((resolve, reject) => {
subProcessOfPuppeteer!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => {
const data = raw
switch (data.type) {
case 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED': {
resolve(data)
break
}
case 'LOGIN_STATUS_INVALID': {
await sleep(500)
mainWindow?.webContents.send('check-boss-zhipin-cookie-file')
return
}
default: {
return
}
}
})
subProcessOfPuppeteer!.once('exit', (exitCode) => {
subProcessOfPuppeteer = null
if (exitCode === AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) {
// means cannot find downloaded puppeteer
reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
} else {
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
}
})
})
// TODO:
return { runRecordId }
})
ipcMain.handle('run-read-no-reply-auto-reminder', async () => {
if (subProcessOfPuppeteer) {
return
}
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
}
const subProcessEnv = {
...process.env,
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
}
subProcessOfPuppeteer = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=readNoReplyAutoReminder`],
{
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
const mode = 'readNoReplyAutoReminderMain'
const { runRecordId } = await runCommon({ mode })
daemonEE.on('message', function handler(message) {
if (message.workerId !== mode) {
return
}
)
// console.log(subProcessOfPuppeteer)
return new Promise((resolve, reject) => {
subProcessOfPuppeteer!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => {
const data = raw
switch (data.type) {
case 'LOGIN_STATUS_INVALID': {
await sleep(500)
mainWindow?.webContents.send('check-boss-zhipin-cookie-file')
return
}
case 'ERR_INTERNET_DISCONNECTED': {
mainWindow?.webContents.send('toast-message', {
type: 'error',
message: '联网失败,请检查网络连接'
})
return
}
default: {
return
}
}
})
subProcessOfPuppeteer!.once('exit', (exitCode) => {
subProcessOfPuppeteer = null
if (exitCode === AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) {
// means cannot find downloaded puppeteer
reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
} else {
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
}
})
resolve(true)
})
// TODO:
})
ipcMain.handle('check-dependencies', async () => {
const [anyAvailablePuppeteerExecutable] = await Promise.all([
getAnyAvailablePuppeteerExecutable()
])
return {
puppeteerExecutableAvailable: !!anyAvailablePuppeteerExecutable
}
})
let subProcessOfCheckAndDownloadDependencies: ChildProcess | null = null
ipcMain.handle('setup-dependencies', async () => {
if (subProcessOfCheckAndDownloadDependencies) {
return
}
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=checkAndDownloadDependenciesForInit`],
{
stdio: [null, null, null, 'pipe', 'ipc']
if (message.type === 'worker-exited') {
mainWindow?.webContents.send('worker-exited', message)
}
)
return new Promise((resolve, reject) => {
subProcessOfCheckAndDownloadDependencies!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'NEED_RESETUP_DEPENDENCIES':
case 'PUPPETEER_DOWNLOAD_PROGRESS': {
mainWindow?.webContents.send(data.type, data)
break
}
case 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR': {
console.error(data)
break
}
default: {
return
}
}
}
)
subProcessOfCheckAndDownloadDependencies!.once('exit', (exitCode) => {
switch (exitCode) {
case 0: {
resolve(exitCode)
break
}
default: {
reject('PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR')
break
}
}
subProcessOfCheckAndDownloadDependencies = null
})
})
return { runRecordId }
})
ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => {
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping')
subProcessOfPuppeteer?.kill()
setTimeout(() => {
try {
subProcessOfPuppeteer?.kill('SIGKILL')
} catch {
//
}
}, 1000)
})
let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null
ipcMain.on('launch-bosszhipin-login-page-with-preload-extension', async () => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
}
const subProcessEnv = {
...process.env,
PUPPETEER_EXECUTABLE_PATH: (await getAnyAvailablePuppeteerExecutable())!.executablePath
}
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=launchBossZhipinLoginPageWithPreloadExtension`],
{
env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc']
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'BOSS_ZHIPIN_COOKIE_COLLECTED': {
mainWindow?.webContents.send(data.type, data)
break
}
default: {
return
}
const p = new Promise((resolve) => {
daemonEE.on('message', function handler(message) {
if (message.workerId !== 'geekAutoStartWithBossMain') {
return
}
if (message.type === 'worker-exited') {
daemonEE.off('message', handler)
resolve(undefined)
}
})
})
await sendToDaemon(
{
type: 'stop-worker',
workerId: 'geekAutoStartWithBossMain'
},
{
needCallback: true
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.once('exit', () => {
mainWindow?.webContents.send('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED')
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
})
await p
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
})
ipcMain.on('kill-bosszhipin-login-page-with-preload-extension', async () => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
} finally {
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
}
ipcMain.handle('stop-read-no-reply-auto-reminder', async () => {
mainWindow?.webContents.send('read-no-reply-auto-reminder-stopping')
const p = new Promise((resolve) => {
daemonEE.on('message', function handler(message) {
if (message.workerId !== 'readNoReplyAutoReminderMain') {
return
}
if (message.type === 'worker-exited') {
daemonEE.off('message', handler)
resolve(undefined)
}
})
})
await sendToDaemon(
{
type: 'stop-worker',
workerId: 'readNoReplyAutoReminderMain'
},
{
needCallback: true
}
)
await p
mainWindow?.webContents.send('read-no-reply-auto-reminder-stopped')
})
ipcMain.handle('get-task-manager-list', async () => {
const result = await sendToDaemon(
{
type: 'get-status'
},
{
needCallback: true
}
)
return result
})
// IPC处理停止工具进程
ipcMain.handle('stop-task', async (_, workerId) => {
await sendToDaemon(
{
type: 'stop-worker',
workerId
},
{
needCallback: true
}
)
})
ipcMain.handle('check-boss-zhipin-cookie-file', () => {
@@ -473,7 +334,7 @@ export default function initIpc() {
let subProcessOfOpenBossSiteDefer: null | PromiseWithResolvers<ChildProcess> = null
let subProcessOfOpenBossSite: null | ChildProcess = null
ipcMain.handle('open-site-with-boss-cookie', async (_, data) => {
ipcMain.handle('open-site-with-boss-cookie', async (ev, data) => {
const url = data.url
if (
!subProcessOfOpenBossSiteDefer ||
@@ -481,7 +342,31 @@ export default function initIpc() {
subProcessOfOpenBossSite.killed
) {
subProcessOfOpenBossSiteDefer = Promise.withResolvers()
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
const parent = BrowserWindow.fromWebContents(ev.sender) || undefined
await configWithBrowserAssistant({
autoFind: true,
windowOption: {
parent,
modal: !!parent,
show: true
}
})
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
} catch (error) {
//
}
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
return
}
const subProcessEnv = {
...process.env,
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable!.executablePath
@@ -666,6 +551,60 @@ export default function initIpc() {
const newRelease = await checkUpdateForUi()
return newRelease
})
ipcMain.handle('login-with-cookie-assistant', async () => {
return await loginWithCookieAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
})
ipcMain.handle('config-with-browser-assistant', async () => {
return await configWithBrowserAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
})
ipcMain.handle('pre-enter-setting-ui', async () => {
if (!isFirstLaunchNoticeApproveFlagExist()) {
try {
await waitForUserApproveAgreement({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
createFirstLaunchNoticeApproveFlag()
} catch {
app.exit(0)
return
}
}
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
const lastBrowser = await getLastUsedAndAvailableBrowser()
if (!lastBrowser) {
try {
await configWithBrowserAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
},
autoFind: true
})
} catch (err) {
void err
}
}
}
})
ipcMain.handle('exit-app-immediately', () => {
app.exit(0)

View File

@@ -0,0 +1,91 @@
import { spawn } from 'child_process'
import {
ensureStorageFileExist,
writeStorageFile,
readStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { randomUUID } from 'node:crypto'
import { connectToDaemon } from './connect-to-daemon'
const isUiDev = process.env.NODE_ENV === 'development'
export async function ensureIpcPipeName({ isReset } = {}) {
if (isReset) {
await writeStorageFile('ipc-pipe-name', '', { isJson: false })
}
let ipcPipeName = readStorageFile('ipc-pipe-name', { isJson: false })
if (!ipcPipeName) {
ipcPipeName = `geekgeekrun-d_${randomUUID()}`
ensureStorageFileExist()
await writeStorageFile('ipc-pipe-name', ipcPipeName, { isJson: false })
}
process.env.GEEKGEEKRUND_PIPE_NAME = ipcPipeName
return ipcPipeName
}
export async function launchDaemon() {
let daemonProcess
async function startDaemon() {
console.log('启动守护进程...')
// 添加参数使守护进程在后台运行,不显示 UI
daemonProcess = spawn(
process.argv[0],
isUiDev ? [process.argv[1], `--mode=launchDaemon`] : [`--mode=launchDaemon`],
{
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
detached: true,
env: {
...process.env
}
}
)
daemonProcess.stdout.on('data', (data) => {
console.log(`守护进程输出: ${data}`)
})
daemonProcess.stderr.on('data', (data) => {
console.error(`守护进程错误: ${data}`)
})
return new Promise((resolve, reject) => {
daemonProcess.stdio[3].on('data', (rawData) => {
let data
try {
data = JSON.parse(rawData.toString())
if (data.type === 'DAEMON_READY') {
resolve(true)
} else if (data.type === 'DAEMON_FATAL') {
reject(new Error(data.error))
}
} catch (err) {
console.error('', err)
}
})
})
}
await ensureIpcPipeName()
try {
await connectToDaemon()
} catch (err) {
let isDaemonLaunched = false
console.log('cannot connect to daemon, try to launch it', err)
// 启动守护进程
try {
await startDaemon()
isDaemonLaunched = true
} catch (err) {
console.log('cannot launch to daemon, try to change port', err)
daemonProcess?.kill('SIGKILL')
await ensureIpcPipeName({ isReset: true })
try {
await startDaemon()
isDaemonLaunched = true
} catch (err) {
console.log('cannot launch to daemon, try to change port failed', err)
}
}
if (isDaemonLaunched) {
await connectToDaemon()
}
}
}

View File

@@ -20,7 +20,7 @@ export const initDbWorker = () => {
// attach more event
worker?.off('message', handler)
} else if (data.type === 'DB_INIT_FAIL') {
reject(undefined)
reject(data.error)
worker?.terminate()
worker?.off('message', handler)
worker = null
@@ -102,3 +102,10 @@ export const getJobHistoryByEncryptId = async (encryptJobId) => {
})
return res
}
export const saveAndGetCurrentRunRecord = async () => {
const res = await createWorkerPromise({
type: 'saveAndGetCurrentRunRecord'
})
return res
}

View File

@@ -11,6 +11,7 @@ import { VMarkAsNotSuitLog } from '@geekgeekrun/sqlite-plugin/dist/entity/VMarkA
import { measureExecutionTime } from '../../../../../../common/utils/performance'
import { PageReq, PagedRes } from '../../../../../../common/types/pagination'
import { JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/dist/entity/JobInfoChangeLog'
import { AutoStartChatRunRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/AutoStartChatRunRecord'
const dbInitPromise = initDb(getPublicDbFilePath())
let dataSource: DataSource | null = null
@@ -161,6 +162,13 @@ const payloadHandler = {
})
)
return data
},
async saveAndGetCurrentRunRecord() {
const autoStartChatRunRecord = new AutoStartChatRunRecord()
autoStartChatRunRecord.date = new Date()
const autoStartChatRunRecordRepository = dataSource!.getRepository(AutoStartChatRunRecord)
const result = await autoStartChatRunRecordRepository.save(autoStartChatRunRecord)
return result
}
}

View File

@@ -7,8 +7,6 @@ import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storag
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
const bossChatUiUrl = `https://www.zhipin.com/web/geek/chat`
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
export async function bootstrap() {
const { puppeteer } = await initPuppeteer()
@@ -28,6 +26,8 @@ export async function bootstrap() {
export async function launchBoss(browser: Browser) {
const page = (await browser.pages())[0]
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
//set cookies
for (let i = 0; i < bossCookies.length; i++) {
await page.setCookie(bossCookies[i])

View File

@@ -4,7 +4,6 @@ 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'
import {
@@ -17,16 +16,35 @@ import {
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'
import {
writeStorageFile,
readStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
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 {
AUTO_CHAT_ERROR_EXIT_CODE,
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'
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
// import { pushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
import minimist from 'minimist'
import { checkCookieListFormat } from '../../../common/utils/cookie'
import { loginWithCookieAssistant } from '../../features/login-with-cookie-assistant'
import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在退出')
process.exit(0)
})
const throttleIntervalMinutes =
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
@@ -42,7 +60,23 @@ const rechatLlmFallback =
const expectJobTypeRegExpStr = readConfigFile('boss.json').expectJobTypeRegExpStr
const onlyRemindBossWithExpectJobType =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithExpectJobType ?? !!expectJobTypeRegExpStr
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithExpectJobType ??
!!expectJobTypeRegExpStr
const blockCompanyNameRegExpStr = readConfigFile('boss.json').blockCompanyNameRegExpStr ?? ''
const blockCompanyNameRegExp = (() => {
if (!blockCompanyNameRegExpStr?.trim()) {
return null
}
try {
return new RegExp(blockCompanyNameRegExpStr, 'im')
} catch {
return null
}
})()
const onlyRemindBossWithoutBlockCompanyName =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithoutBlockCompanyName ??
!!blockCompanyNameRegExp
const dbInitPromise = initDb(getPublicDbFilePath())
@@ -50,6 +84,30 @@ export const pageMapByName: {
boss?: Page | null
} = {}
// async function periodPushCurrentPageScreenshot () {
// try {
// if (pageMapByName.boss?.isClosed()) {
// return
// }
// const shouldExit = await checkShouldExit()
// if (shouldExit) {
// return
// }
// try {
// await pushCurrentPageScreenshot(pageMapByName.boss)
// }
// catch (err) {
// if (err?.message?.includes(`PAGE_CLOSED`)) {
// return
// }
// }
// setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
// }
// catch {}
// }
// periodPushCurrentPageScreenshot()
async function saveCurrentChatRecord(page) {
const userInfo = await page.evaluate(
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
@@ -114,7 +172,8 @@ async function saveCurrentChatRecord(page) {
async function checkJobIsClosed() {
const encryptJobId = await pageMapByName.boss!.evaluate(() => {
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$.encryptJobId
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$
.encryptJobId
})
if (!encryptJobId) {
return false
@@ -214,6 +273,44 @@ const mainLoop = async () => {
browser = null
}
}
let bossCookies = readStorageFile('boss-cookies.json')
let cookieCheckResult = checkCookieListFormat(bossCookies)
while (!cookieCheckResult) {
try {
await loginWithCookieAssistant()
bossCookies = readStorageFile('boss-cookies.json')
cookieCheckResult = checkCookieListFormat(bossCookies)
} catch (err) {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录BOSS直聘`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'fulfilled'
},
runRecordId
}
})
const canNotConfirmIfHasReadMsgTemplateList = [
'Boss还没查看你的消息',
'你与该职位竞争者PK情况',
@@ -232,12 +329,45 @@ const mainLoop = async () => {
// #region
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
writeStorageFile('boss-cookies.json', [])
throw new Error('LOGIN_STATUS_INVALID')
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
} catch (err) {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录BOSS直聘`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
throw new Error('THROW_FOR_RETRY')
}
if (
currentPageUrl.startsWith('https://www.zhipin.com/web/common/403.html') ||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/error.html')
) {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('ACCESS_IS_DENIED')
}
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/safe/verify-slider')) {
@@ -260,9 +390,31 @@ const mainLoop = async () => {
})
if (validateRes.code === 0) {
await storeStorage(pageMapByName.boss)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('CAPTCHA_PASSED_AND_NEED_RESTART')
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'fulfilled'
},
runRecordId
}
})
// #endregion
// check set security question tip modal
let setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$(
@@ -296,6 +448,9 @@ const mainLoop = async () => {
const toCheckItemAtIndex = friendListData.findIndex((it, index) => {
return (
index >= cursorToContinueFind &&
(onlyRemindBossWithoutBlockCompanyName && blockCompanyNameRegExp
? !blockCompanyNameRegExp.test(it.brandName)
: true) &&
(rechatLimitDay && it.updateTime
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
: true) &&
@@ -452,10 +607,6 @@ const mainLoop = async () => {
}
}
let isParentProcessDisconnect = false
process.once('disconnect', () => {
isParentProcessDisconnect = true
})
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
@@ -465,19 +616,75 @@ const rerunInterval = (() => {
return v
})()
let pipe
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.warn('pipe is not available')
}
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
export async function runEntry() {
process.on('disconnect', () => {
app.exit()
})
app.dock?.hide()
while (!isParentProcessDisconnect) {
await app.whenReady()
app.on('window-all-closed', (e) => {
e.preventDefault()
})
initPublicIpc()
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'worker-launch',
status: 'fulfilled'
},
runRecordId
}
})
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error(`PUPPETEER_IS_NOT_EXECUTABLE`)
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'fulfilled'
},
runRecordId
}
})
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
while (true) {
try {
await mainLoop()
} catch (err) {
@@ -487,31 +694,44 @@ export async function runEntry() {
} catch {
//
}
// handle error
if (
err instanceof Error &&
['LOGIN_STATUS_INVALID', 'ACCESS_IS_DENIED', 'ERR_INTERNET_DISCONNECTED'].includes(
err.message
)
) {
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: err.message
}) + '\r\n'
)
process.exit(1)
const shouldExit = await checkShouldExit()
if (shouldExit) {
app.exit()
return
}
if (err instanceof Error && err.message === 'CANNOT_FIND_A_USABLE_MODEL') {
gtag('cannot_find_a_usable_model')
await dialog.showMessageBox({
type: 'error',
message:
'未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题',
buttons: ['退出']
})
process.exit(0)
break;
// handle error
if (err instanceof Error) {
if (err.message.includes('LOGIN_STATUS_INVALID')) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
break
}
if (err.message.includes('ERR_INTERNET_DISCONNECTED')) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ERR_INTERNET_DISCONNECTED)
break
}
if (err.message.includes('ACCESS_IS_DENIED')) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED)
break
}
if (
err.message.includes(`PUPPETEER_IS_NOT_EXECUTABLE`) ||
err.message.includes(`Could not find Chrome`) ||
err.message.includes(`no executable was found`)
) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
break
}
if (err.message === 'CANNOT_FIND_A_USABLE_MODEL') {
gtag('cannot_find_a_usable_model')
await dialog.showMessageBox({
type: 'error',
message:
'未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题',
buttons: ['退出']
})
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE)
break
}
}
} finally {
pageMapByName['boss'] = null
@@ -522,8 +742,6 @@ export async function runEntry() {
process.exit(0)
}
attachListenerForKillSelfOnParentExited()
process.once('uncaughtException', (error) => {
console.error('uncaughtException', error)
process.exit(1)
@@ -533,6 +751,10 @@ process.once('unhandledRejection', (error) => {
process.exit(1)
})
process.once('disconnect', () => {
process.exit(0)
})
async function storeStorage(page) {
const [cookies, localStorage] = await Promise.all([
page.cookies(),

View File

@@ -1,12 +1,25 @@
import minimist from 'minimist'
import { runCommon } from './features/run-common'
import { launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'
import { app } from 'electron'
// 捕获未处理的 EPIPE 错误
process.on('uncaughtException', (err) => {
if (err?.code === 'EPIPE' || err?.code === 'ERR_STREAM_DESTROYED') {
return
}
throw err
})
const isUiDev = process.env.NODE_ENV === 'development'
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
console.log(commandlineArgs)
const runMode = commandlineArgs['mode'];
const runMode = commandlineArgs['mode']
;(async () => {
switch (runMode) {
// #region internal use
case 'geekAutoStartWithBossMain': {
const { waitForProcessHandShakeAndRunAutoChat } = await import(
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index'
@@ -14,18 +27,9 @@ const runMode = commandlineArgs['mode'];
waitForProcessHandShakeAndRunAutoChat()
break
}
case 'geekAutoStartWithBossDaemon': {
const { runAutoChatWithDaemon } = await import(
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/index'
)
runAutoChatWithDaemon()
break
}
case 'checkAndDownloadDependenciesForInit': {
const { checkAndDownloadDependenciesForInit } = await import(
'./flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/index'
)
checkAndDownloadDependenciesForInit()
case 'downloadDependenciesForInit': {
const { downloadDependenciesForInit } = await import('./flow/DOWNLOAD_DEPENDENCIES/index')
downloadDependenciesForInit()
break
}
case 'launchBossZhipinLoginPageWithPreloadExtension': {
@@ -40,15 +44,43 @@ const runMode = commandlineArgs['mode'];
launchBossSite()
break
}
case 'readNoReplyAutoReminder': {
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER/index')
case 'readNoReplyAutoReminderMain': {
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER_MAIN/index')
runEntry()
break
}
case 'launchDaemon': {
await import('./flow/LAUNCH_DAEMON')
break
}
// #endregion
// #region user entry
case 'geekAutoStartWithBoss': {
app.dock?.hide()
await launchDaemon()
const { isAlreadyRunning } = await runCommon({ mode: 'geekAutoStartWithBossMain' })
if (isAlreadyRunning) {
process.exit(0)
}
break
}
case 'readNoReplyAutoReminder': {
app.dock?.hide()
await launchDaemon()
const { isAlreadyRunning } = await runCommon({ mode: 'readNoReplyAutoReminderMain' })
if (isAlreadyRunning) {
process.exit(0)
}
break
}
default: {
globalThis.GEEKGEEKRUN_PROCESS_ROLE = 'ui'
await launchDaemon()
const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index')
openSettingWindow()
break
}
// #region
}
})()

View File

@@ -1,7 +1,13 @@
import { BrowserWindow, ipcMain, shell } from 'electron'
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
import gtag from './gtag'
import buildInfo from '../../common/build-info.json'
import os from 'node:os'
import fs from 'node:fs'
import {
ensureStorageFileExist,
readStorageFile,
writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
export default function initPublicIpc() {
ipcMain.on(
@@ -56,4 +62,52 @@ export default function initPublicIpc() {
}
)
})
ipcMain.handle('read-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await readStorageFile(payload.fileName)
})
ipcMain.handle('write-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
})
ipcMain.handle('get-os-platform', () => {
return os.platform()
})
ipcMain.handle('choose-file', (ev, { fileChooserConfig }) => {
if (!fileChooserConfig) {
fileChooserConfig = {}
}
const win = BrowserWindow.fromWebContents(ev.sender)
if (!win) {
return dialog.showOpenDialog(fileChooserConfig)
} else {
return dialog.showOpenDialog(win, fileChooserConfig)
}
})
ipcMain.handle('check-executable-file', (ev, filePath: string) => {
if (!filePath?.trim()) {
return {
message: '文件名无效'
}
}
if (!fs.existsSync(filePath)) {
return {
message: '文件不存在'
}
}
if (!fs.statSync(filePath).isFile()) {
const messageSeg = ['文件不是可执行文件']
if (os.platform() === 'darwin') {
messageSeg.push(
'macOS 平台可执行文件位于“App包.app/Contents/MacOS/ 文件夹下”而不是“App包.app”文件夹整体'
)
}
return {
message: messageSeg.join('')
}
}
return null
})
}

View File

@@ -0,0 +1,67 @@
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
import { checkShouldExit } from "./worker"
export const SCREENSHOT_INTERVAL_MS = 2500
export async function pushCurrentPageScreenshot (page) {
try {
if (!page) {
return
}
// 尝试截图当前页面(压缩为 jpeg + base64避免文件写盘
const screenshotBase64 = await page.screenshot({
type: 'jpeg',
quality: 60,
encoding: 'base64',
fullPage: false
})
const screenshotAt = Date.now()
await sendToDaemon({
type: 'worker-screenshot',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
screenshot: `data:image/jpeg;base64,${screenshotBase64}`,
screenshotAt,
pageUrl: page.url?.() ?? null
}
})
} catch (err) {
if (err?.message?.includes('Session closed')) {
throw new Error(`PAGE_CLOSED`)
}
// 截图失败不应影响主流程
console.warn('[pushCurrentPageScreenshot] error', err)
}
}
export class PeriodPushCurrentPageScreenshotPlugin {
apply(hooks) {
hooks.pageGotten.tap(
'PeriodPushCurrentPageScreenshotPlugin',
(page) => {
async function periodPushCurrentPageScreenshot () {
try {
if (page.isClosed()) {
return
}
const shouldExit = await checkShouldExit()
if (shouldExit) {
return
}
try {
await pushCurrentPageScreenshot(page)
}
catch (err) {
if (err?.message?.includes(`PAGE_CLOSED`)) {
return
}
}
setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
}
catch {}
}
periodPushCurrentPageScreenshot()
}
)
}
}

View File

@@ -0,0 +1,14 @@
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
export async function checkShouldExit () {
const shouldExitResponse = await sendToDaemon(
{
type: 'check-should-exit',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
},
{
needCallback: true
}
)
return shouldExitResponse?.shouldExit
}

View File

@@ -0,0 +1,100 @@
import { BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import { getAnyAvailablePuppeteerExecutable } from '../flow/DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
import {
getLastUsedAndAvailableBrowser,
saveLastUsedAndAvailableBrowserInfo
} from '../flow/DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { openBrowserDownloadWindow } from '../features/open-browser-download-window'
export let browserAssistantWindow: BrowserWindow | null = null
const registerHandleWithWindow = (
win: BrowserWindow,
...args: Parameters<typeof ipcMain.handle>
) => {
const [channel, handler] = args
ipcMain.handle(channel, handler)
win.once('closed', () => ipcMain.removeHandler(channel))
}
export function createBrowserAssistantWindow(
opt?: Electron.BrowserWindowConstructorOptions,
{ autoFind } = {}
): BrowserWindow {
// Create the browser window.
if (browserAssistantWindow) {
browserAssistantWindow!.close()
}
browserAssistantWindow = new BrowserWindow({
width: 800,
minWidth: 800,
height: 400,
resizable: true,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
browserAssistantWindow.on('ready-to-show', () => {
browserAssistantWindow!.show()
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
let routePath = '#/browserAssistant'
if (autoFind) {
routePath = '#/browserAutoFind'
}
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
browserAssistantWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + routePath)
} else {
browserAssistantWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + routePath
)
}
browserAssistantWindow!.once('closed', () => {
browserAssistantWindow = null
})
registerHandleWithWindow(
browserAssistantWindow,
'get-any-available-puppeteer-executable',
async (_, { ignoreCached, noSave } = {}) => {
return await getAnyAvailablePuppeteerExecutable({ ignoreCached, noSave })
}
)
registerHandleWithWindow(
browserAssistantWindow,
'get-last-used-and-available-browser',
async () => {
return await getLastUsedAndAvailableBrowser()
}
)
registerHandleWithWindow(
browserAssistantWindow,
'save-last-used-and-available-browser-info',
async (_, payload) => {
return await saveLastUsedAndAvailableBrowserInfo(payload)
}
)
registerHandleWithWindow(browserAssistantWindow, 'download-browser-with-downloader', async () => {
return await openBrowserDownloadWindow({
windowOption: {
parent: browserAssistantWindow!,
modal: true,
show: true
}
})
})
return browserAssistantWindow!
}

View File

@@ -0,0 +1,124 @@
import { ChildProcess } from 'child_process'
import { BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import * as childProcess from 'node:child_process'
import * as JSONStream from 'JSONStream'
import * as fs from 'node:fs'
import { cacheDir } from '../constant'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../common/constant'
import * as puppeteerManager from '@puppeteer/browsers'
export let browserDownloadProgressWindow: BrowserWindow | null = null
const registerHandleWithWindow = (
win: BrowserWindow,
...args: Parameters<typeof ipcMain.handle>
) => {
const [channel, handler] = args
ipcMain.handle(channel, handler)
win.once('closed', () => ipcMain.removeHandler(channel))
}
export function createBrowserDownloadProgressWindow(
opt?: Electron.BrowserWindowConstructorOptions
): BrowserWindow {
// Create the browser window.
if (browserDownloadProgressWindow) {
browserDownloadProgressWindow!.close()
}
browserDownloadProgressWindow = new BrowserWindow({
width: 600,
height: 200,
resizable: false,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
browserDownloadProgressWindow.on('ready-to-show', () => {
browserDownloadProgressWindow!.show()
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
browserDownloadProgressWindow.loadURL(
process.env['ELECTRON_RENDERER_URL'] + '#/browserDownloadProgress'
)
} else {
browserDownloadProgressWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + '#/browserDownloadProgress'
)
}
let subProcessOfCheckAndDownloadDependencies: ChildProcess | null = null
registerHandleWithWindow(browserDownloadProgressWindow, 'setup-dependencies', async () => {
if (subProcessOfCheckAndDownloadDependencies) {
return
}
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=downloadDependenciesForInit`],
{
stdio: [null, null, null, 'pipe', 'ipc']
}
)
return new Promise((resolve, reject) => {
subProcessOfCheckAndDownloadDependencies!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'NEED_RESETUP_DEPENDENCIES':
case 'PUPPETEER_DOWNLOAD_PROGRESS': {
browserDownloadProgressWindow?.webContents.send(data.type, data)
break
}
case 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR': {
console.error(data)
break
}
default: {
return
}
}
}
)
subProcessOfCheckAndDownloadDependencies!.once('exit', (exitCode) => {
const executablePath = puppeteerManager.computeExecutablePath({
browser: puppeteerManager.Browser.CHROME,
cacheDir,
buildId: EXPECT_CHROMIUM_BUILD_ID
})
if (exitCode === 0 && fs.existsSync(executablePath)) {
resolve(executablePath)
} else {
reject('PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR')
}
subProcessOfCheckAndDownloadDependencies = null
})
})
})
const killHandler = async () => {
try {
subProcessOfCheckAndDownloadDependencies?.kill()
} catch {
//
} finally {
subProcessOfCheckAndDownloadDependencies = null
}
}
browserDownloadProgressWindow.once('closed', () => {
killHandler()
})
browserDownloadProgressWindow.once('closed', () => {
browserDownloadProgressWindow = null
})
return browserDownloadProgressWindow!
}

View File

@@ -0,0 +1,132 @@
import { ChildProcess } from 'child_process'
import { BrowserWindow, dialog, ipcMain } from 'electron'
import path from 'path'
import * as childProcess from 'node:child_process'
import * as JSONStream from 'JSONStream'
import { getLastUsedAndAvailableBrowser } from '../flow/DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../features/config-with-browser-assistant'
export let cookieAssistantWindow: BrowserWindow | null = null
export function createCookieAssistantWindow(
opt?: Electron.BrowserWindowConstructorOptions
): BrowserWindow {
// Create the browser window.
if (cookieAssistantWindow) {
cookieAssistantWindow!.show()
}
cookieAssistantWindow = new BrowserWindow({
width: 960,
height: 720,
resizable: true,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
cookieAssistantWindow.on('ready-to-show', () => {
cookieAssistantWindow!.show()
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
cookieAssistantWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/cookieAssistant')
} else {
cookieAssistantWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + '#/cookieAssistant'
)
}
cookieAssistantWindow!.once('closed', () => {
cookieAssistantWindow = null
})
let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null
const launchHandler = async (ev) => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
}
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
const parent = BrowserWindow.fromWebContents(ev.sender) || undefined
await configWithBrowserAssistant({
autoFind: true,
windowOption: {
parent,
modal: !!parent,
show: true
}
})
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
return
}
const subProcessEnv = {
...process.env,
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
}
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=launchBossZhipinLoginPageWithPreloadExtension`],
{
env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc']
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'BOSS_ZHIPIN_COOKIE_COLLECTED': {
cookieAssistantWindow?.webContents.send(data.type, data)
break
}
default: {
return
}
}
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.once('exit', () => {
cookieAssistantWindow?.webContents.send('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED')
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
})
}
ipcMain.on('launch-bosszhipin-login-page-with-preload-extension', launchHandler)
const killHandler = async () => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
} finally {
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
}
}
ipcMain.on('kill-bosszhipin-login-page-with-preload-extension', killHandler)
cookieAssistantWindow.on('closed', () => {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
ipcMain.off('launch-bosszhipin-login-page-with-preload-extension', launchHandler)
ipcMain.off('kill-bosszhipin-login-page-with-preload-extension', killHandler)
})
return cookieAssistantWindow!
}

View File

@@ -1,6 +1,5 @@
import { BrowserWindow, ipcMain } from 'electron'
import { BrowserWindow } from 'electron'
import path from 'path'
import { createFirstLaunchNoticeApproveFlag } from '../features/first-launch-notice-window'
export let firstLaunchNoticeWindow: BrowserWindow | null = null
export function createFirstLaunchNoticeWindow(
@@ -41,11 +40,3 @@ export function createFirstLaunchNoticeWindow(
return firstLaunchNoticeWindow!
}
export const initIpc = () => {
ipcMain.handle('first-launch-notice-approve', () => {
createFirstLaunchNoticeApproveFlag()
firstLaunchNoticeWindow?.close()
})
}
initIpc()

View File

@@ -1,8 +1,7 @@
import { BrowserWindow, shell } from 'electron'
import path from 'path'
import { openDevTools } from '../commands'
import { createFirstLaunchNoticeWindow } from './firstLaunchNoticeWindow'
import { isFirstLaunchNoticeApproveFlagExist } from '../features/first-launch-notice-window'
import { daemonEE } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
export let mainWindow: BrowserWindow | null = null
export function createMainWindow(): BrowserWindow {
@@ -27,14 +26,6 @@ export function createMainWindow(): BrowserWindow {
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.on('ready-to-show', async () => {
!isFirstLaunchNoticeApproveFlagExist() &&
createFirstLaunchNoticeWindow({
parent: mainWindow!,
modal: true,
show: true
})
})
mainWindow.on('ready-to-show', async () => {
process.env.NODE_ENV === 'development' &&
setTimeout(() => {
@@ -58,5 +49,13 @@ export function createMainWindow(): BrowserWindow {
mainWindow!.once('closed', () => {
mainWindow = null
})
daemonEE.on('message', (message) => {
if (message.type === 'worker-to-gui-message') {
mainWindow?.webContents?.send('worker-to-gui-message', message)
}
})
daemonEE.on('error', (err) => {
console.log(err)
})
return mainWindow!
}

View File

@@ -11,7 +11,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { random } from 'lodash-es'
import { random } from 'lodash'
const rowCount = 4
const colCount = 6

View File

@@ -5,7 +5,7 @@
<el-col :span="6"><el-form-item label="公司">{{ jobInfo.companyName }}</el-form-item></el-col>
<el-col :span="6"><el-form-item label="职位名称">{{ jobInfo.jobName }}</el-form-item></el-col>
<el-col :span="6"><el-form-item label="职位分类">{{ jobInfo.positionName }}</el-form-item></el-col>
<el-col :span="6"><el-form-item label="Boss及其身份">{{ jobInfo.bossName }} {{ jobInfo.bossTitle }}</el-form-item></el-col>
<el-col :span="6"><el-form-item label="BOSS及其身份">{{ jobInfo.bossName }} {{ jobInfo.bossTitle }}</el-form-item></el-col>
</el-row>
</el-form>
<el-divider content-position="left">变更记录</el-divider>

View File

@@ -0,0 +1,166 @@
<template>
<el-dialog
modal-class="runing-overlay__modal"
:model-value="isDialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
>
<!-- v-if="stepsForRender.some(it => ['todo', 'pending', 'rejected'].includes(it.status))" -->
<div>
<ul m0 pl0>
<li
v-for="(item, index) in stepsForRender"
:key="index"
list-style-none
flex
justify-start
pt4px
pb4px
>
<div>
<span v-if="item.status === 'todo'">🕐</span>
<span v-if="item.status === 'pending'">👉</span>
<span v-if="item.status === 'fulfilled'"></span>
<span v-if="item.status === 'rejected'"></span>
</div>
<span ml8px>{{ item.describe }}</span>
</li>
</ul>
</div>
<div flex justify-between items-center w-full>
<div>
{{ runningStatusTextMapByCode[currentRunningStatus] }}
</div>
<div>
<slot name="op-buttons" :current-running-status="currentRunningStatus" />
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
// import { useTaskManagerStore } from '@renderer/store'
import { getAutoStartChatSteps } from '../../../../common/prerequisite-step-by-step-check'
import { computed, onUnmounted, ref, watch } from 'vue'
import {
AUTO_CHAT_ERROR_EXIT_CODE,
RUNNING_STATUS_ENUM
} from '../../../../common/enums/auto-start-chat'
const props = defineProps({
workerId: {
type: String
},
runRecordId: {
type: Number
}
})
// const taskManagerStore = useTaskManagerStore()
// const runingTaskInfo = computed(() => {
// return taskManagerStore.runningTasks?.find((it) => {
// return it.workerId === props.workerId
// })
// })
const steps = ref([])
const stepsForRender = computed(() => {
const clonedSteps = JSON.parse(JSON.stringify(steps.value))
if (clonedSteps.some((it) => it.status === 'rejected')) {
return clonedSteps
}
const lastFulfilledIndex = clonedSteps.findLastIndex((it) => it.status === 'fulfilled')
if (lastFulfilledIndex + 1 < clonedSteps.length) {
clonedSteps[lastFulfilledIndex + 1].status = 'pending'
}
return clonedSteps
})
const runningStatusTextMapByCode = {
[RUNNING_STATUS_ENUM.RUNNING]: '正在运行中',
[RUNNING_STATUS_ENUM.NORMAL_EXITED]: '程序已正常退出',
[RUNNING_STATUS_ENUM.ERROR_EXITED]: '程序异常退出'
}
const currentRunningStatus = ref(RUNNING_STATUS_ENUM.RUNNING)
function fillEmptySteps() {
const arr = getAutoStartChatSteps()
arr.forEach((it) => (it.status = 'todo'))
steps.value = arr
currentRunningStatus.value = RUNNING_STATUS_ENUM.RUNNING
}
watch(() => props.runRecordId, fillEmptySteps, {
immediate: true
})
const { ipcRenderer } = electron
function messageHandler(ev, { data }) {
if (
data.type !== 'prerequisite-step-by-step-checkstep-by-step-check' ||
data.runRecordId !== props.runRecordId
) {
return
}
const { id: stepId, status: stepStatus } = data.step
const targetStep = steps.value.find((it) => it.id === stepId)
if (!targetStep) {
return
}
targetStep.status = stepStatus
}
const unListenMessage = ipcRenderer.on('worker-to-gui-message', messageHandler)
onUnmounted(unListenMessage)
const isDialogVisible = ref(false)
const show = () => {
isDialogVisible.value = true
}
const hide = () => {
isDialogVisible.value = false
}
defineExpose({
show,
hide
})
ipcRenderer.on('worker-exited', (ev, payload) => {
const { workerId, code } = payload
if (
workerId !== props.workerId
// || runRecordId !== props.runRecordId
) {
return
}
if (code !== AUTO_CHAT_ERROR_EXIT_CODE.NORMAL) {
currentRunningStatus.value = RUNNING_STATUS_ENUM.ERROR_EXITED
} else {
currentRunningStatus.value = RUNNING_STATUS_ENUM.NORMAL_EXITED
}
})
</script>
<style lang="scss">
.el-overlay.runing-overlay__modal {
position: absolute;
width: 100%;
height: 100%;
backdrop-filter: blur(3px);
background-color: transparent;
background-image: radial-gradient(transparent 1px, #fff 1px);
background-size: 4px 4px;
.el-overlay-dialog {
position: absolute;
pointer-events: all;
}
.el-dialog {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.el-dialog__header {
display: none;
}
.el-dialog__body {
overflow: hidden;
}
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<el-card class="task-item">
<div>
<div flex flex-col position-relative>
<div>
<el-button type="danger" size="small" @click="stopTask(task.workerId)">结束任务</el-button>
</div>
<img block :src="task.screenshot" height="190" width="360" />
<div position-absolute bottom-0 right-0 font-size-12px :style="{
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#fff',
padding: '2px 4px 2px 6px',
borderRadius: '8px 0 0 0'
}">{{ task.screenshotAt ? dayjs(task.screenshotAt).format('YYYY-MM-DD HH:mm:ss') : ' - ' }}</div>
</div>
</div>
<div ml-30px>
<dl>
<dt>workerId</dt>
<dd>{{ task.workerId }}</dd>
</dl>
<dl>
<dt>status</dt>
<dd>{{ task.status }}</dd>
</dl>
<dl>
<dt>重启次数</dt>
<dd>{{ task.restartCount }}</dd>
</dl>
<dl>
<dt>已运行时间</dt>
<dd>{{ task.uptime ?? '-' }} 毫秒</dd>
</dl>
<dl>
<dt>命令行</dt>
<dd>{{ task.command }} {{ task.args.join(' ') }}</dd>
</dl>
<dl>
<dt>PID</dt>
<dd>{{ task.pid }}</dd>
</dl>
</div>
</el-card>
</template>
<script lang="ts" setup>
import dayjs from 'dayjs'
import { PropType } from 'vue'
defineProps({
task: {
type: Object as PropType<any>
}
})
const { ipcRenderer } = electron
const stopTask = async (workerId: string) => {
await ipcRenderer.invoke('stop-task', workerId)
}
</script>
<style lang="scss" scoped>
.task-item {
width: 1000px;
margin: 0 auto;
font-size: 14px;
overflow: hidden;
::v-deep(.el-card__body) {
display: flex;
}
dl {
margin: 0;
display: flex;
padding-top: 4px;
padding-bottom: 4px;
border-bottom: 1px solid #eee;
dt {
width: 6em;
flex: 0 0 6em;
}
dd {
word-break: break-all;
}
}
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<div>
<div v-for="task in runningTasks" :key="task.workerId">
<div>
<TaskManagerItem :task="task" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useTaskManagerStore } from '@renderer/store'
import { computed } from 'vue'
import TaskManagerItem from './Item.vue'
const taskManagerStore = useTaskManagerStore()
const runningTasks = computed(() => taskManagerStore.runningTasks || [])
</script>

View File

@@ -4,56 +4,37 @@
<img
class="block"
:class="{
'animate__animated animate__bounce animate__repeat-3':
Object.values(checkDependenciesResult).includes(false)
'animate__animated animate__bounce animate__repeat-3': true
}"
:width="256"
src="@renderer/../../../resources/icon.png"
/>
</div>
<div mt24px>愿你薪想事成</div>
<div class="h60px mt14px">
<RouterView
class="h100%"
:dependencies-status="checkDependenciesResult"
:process-waitee="downloadProcessWaitee"
></RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { onMounted, ref } from 'vue'
import { onMounted } from 'vue'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { gtagRenderer } from '@renderer/utils/gtag'
const router = useRouter()
const checkDependenciesResult = ref({})
const downloadProcessWaitee = ref(null)
// const checkDependenciesResult = ref({})
// const downloadProcessWaitee = ref(null)
onMounted(async () => {
gtagRenderer('bootstrap_mounted')
checkDependenciesResult.value = await electron.ipcRenderer.invoke('check-dependencies')
downloadProcessWaitee.value = Promise.withResolvers()
if (Object.values(checkDependenciesResult.value).includes(false)) {
gtagRenderer('dependencies_need_download')
router.replace('/downloadingDependencies')
} else {
downloadProcessWaitee.value!.resolve()
await sleep(1500)
try {
await electron.ipcRenderer.invoke('pre-enter-setting-ui')
} catch (err) {
console.log('pre-enter-setting-ui error', err)
} finally {
await sleep(500)
router.replace('/main-layout')
}
downloadProcessWaitee.value!.promise.then(async () => {
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
if (!isCookieFileValid) {
gtagRenderer('found_cookie_invalid_when_bootstrap')
router.replace('/cookieAssistant')
} else {
await sleep(1000)
router.replace('/main-layout')
}
})
})
</script>

View File

@@ -1,107 +0,0 @@
<template>
<div class="flex flex-col flex-items-start flex-justify-start" v-if="!dependenciesStatus.puppeteerExecutableAvailable">
<div mb14px>正在下载兼容的浏览器</div>
<el-progress
:percentage="browserDownloadPercentage"
:format="(n) => `${n.toFixed(1)}%`"
:stroke-width="10"
class="w400px"
/>
</div>
</template>
<script lang="ts" setup>
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: {
type: Object as PropType<Record<string, boolean>>,
default: () => ({})
},
processWaitee: Object
})
const browserDownloadPercentage = ref(0)
const handleBrowserDownloadProgress = (ev, { downloadedBytes, totalBytes }) => {
browserDownloadPercentage.value = (downloadedBytes / totalBytes) * 100
}
electron.ipcRenderer.on('PUPPETEER_DOWNLOAD_PROGRESS', handleBrowserDownloadProgress)
onUnmounted(() =>
electron.ipcRenderer.removeListener('PUPPETEER_DOWNLOAD_PROGRESS', handleBrowserDownloadProgress)
)
const downloadProcessExitCode = ref(0)
const processDownloadBrowser = async () => {
downloadProcessExitCode.value = 0
browserDownloadPercentage.value = 0
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)
}
}
}
const promiseList: Array<Promise<void>> = []
const processTasks = async () => {
if (!props.dependenciesStatus.puppeteerExecutableAvailable) {
gtagRenderer('start_download_puppeteer')
const p = processDownloadBrowser()
promiseList.push(p)
p.then(() => {
gtagRenderer('puppeteer_download_success')
props.dependenciesStatus.puppeteerExecutableAvailable = true
})
}
while (promiseList.length) {
const p = promiseList.shift()!
try {
p.then(() => {
if (!promiseList.length) {
props.processWaitee?.resolve?.()
}
})
await p
} catch {
gtagRenderer('encounter_error_when_download_deps')
await ElMessageBox.confirm(h(FailMessage), {
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false,
type: 'error',
cancelButtonText: '退出程序',
confirmButtonText: '重试'
})
.then(() => {
gtagRenderer('start_retry_download_deps')
processTasks()
})
.catch(() => {
gtagRenderer('cancel_download_deps_and_exit')
promiseList.length = 0
electron.ipcRenderer.invoke('exit-app-immediately')
})
}
}
}
processTasks()
</script>

View File

@@ -0,0 +1,362 @@
<template>
<div class="h-screen of-hidden flex flex-col flex-items-center flex-justify-between">
<div flex-1 of-hidden w-full>
<el-form ref="formRef" :model="formData" :rules="rules" flex flex-col of-hidden h-full>
<div class="bg-#f6f6f6" flex-0>
<el-form-item
class="w-90%"
label="浏览器可执行文件路径"
label-position="top"
prop="browserPath"
pt30px
pb30px
ml-auto
mr-auto
mb-0
>
<div flex flex-1>
<el-input v-model="formData.browserPath" />
<el-button
type="primary"
:loading="isAutoDetectLoading"
@click="autoDetectPuppeteerExecutable"
>自动检测</el-button
>
<el-button :style="{ marginLeft: 0 }" @click="chooseExecutableFile"
>手动选择</el-button
>
</div>
</el-form-item>
</div>
<div flex-1 of-auto font-size-14px line-height-1.5em>
<div mt10px ml-auto mr-auto class="w-90%">
<div>常见问题</div>
<div class="faq-main">
<details class="faq-item">
<summary>自动检测点击后提示未检测到可用浏览器的可执行文件</summary>
<div class="faq-answer">
请尝试如下方案之一来处理
<ul pl1em m0>
<li>
方案一通过本程序下载 Google Chrome for Testing
{{ EXPECT_CHROMIUM_BUILD_ID }} -
<a href="javascript:;" @click="handleClickLaunchBrowserDownloader">点击此处</a
>即可下载这个浏览器仅供本程序使用不会影响到当前 Google Chrome
安装本程序开发过程中主要是使用这个浏览器测试的<span color-orange
>可以保证兼容性</span
>网络波动有一定概率下载失败如多次尝试后确实不能下载成功请尝试方案二<span
color-orange
>推荐</span
>
</li>
<li>
方案二手动安装 Google Chrome 最新版本 -
<a href="javascript:;" @click="handleOpenChromeDownloadPage">点击此处</a>打开
Google Chrome
官方网站找到浏览器下载页面来下载安装程序下载完毕后执行安装程序安装完成后点击上方<a
href="javascript:;"
:loading="isAutoDetectLoading"
@click="autoDetectPuppeteerExecutable"
>自动检测</a
>按钮再次尝试截至本程序开发时2026.2.7Google Chrome 最新版本为
144.0.7559.133
多数情况下本程序都可以正常工作但由于浏览器会自动升级版本不固定可能存在<span
color-orange
>浏览器升级后某些功能不兼容导致本程序不能正确运行</span
>的问题如果您确实遇到不能正常运行的问题<a
href="javascript:;"
@click="handleFeedbackClick"
>提交 Issue</a
>来反馈同时请再尝试方案一
</li>
</ul>
</div>
</details>
<details class="faq-item">
<summary>
如果要手动选择浏览器可选择的浏览器有哪些对于浏览器有什么要求
</summary>
<div class="faq-answer">
<div>
截至本程序开发时2026.2.7已确定支持的各操作系统下的浏览器及版本包括
</div>
<ul>
<li>
<div>macOS</div>
<ul>
<li>Google Chrome for Testing {{ EXPECT_CHROMIUM_BUILD_ID }}</li>
<li>Google Chrome 144.0.7559.133</li>
<li>Microsoft Edge 144.0.3719.115</li>
</ul>
</li>
<li>
<div>Windows</div>
<ul>
<li>Google Chrome for Testing {{ EXPECT_CHROMIUM_BUILD_ID }}</li>
<li>Google Chrome 144.0.7559.133</li>
<li>Microsoft Edge 144.0.3719.93</li>
<li>Opera 127.0.5778.14基于 Chromium 143.0.7499.194</li>
<li>Yandex Browser 25.12.3.1126 基于 Chromium 142.0.7444.1126</li>
</ul>
</li>
<li>
<div>Linux (Ubuntu)</div>
<ul>
<li>Google Chrome for Testing {{ EXPECT_CHROMIUM_BUILD_ID }}</li>
</ul>
</li>
</ul>
<div>
下列浏览器可以使用但由于Chromium内核版本低于本程序设置的版本可能存在潜在问题不一定能完全支持所有功能你可以尝试进行配置
</div>
<ul>
<li>
<div>Windows</div>
<ul>
<li>360 安全浏览器 16.1.2552.64 基于 Chromium 132.0.6834.83</li>
<li>360 极速浏览器X 23.1.1187.64 基于 Chromium 132.0.6805.0</li>
<li>夸克 6.4.0.728 基于 Chromium 130.0.6723.44</li>
</ul>
</li>
</ul>
<div>下列浏览器经过测试已明确不可用</div>
<ul>
<li>
<div>Windows</div>
<ul>
<li>QQ 浏览器 20.1.0 基于 Chromium 116.0.5845.97</li>
<li>搜狗高速浏览器 13.8 基于 Chromium 116.0.5845.97</li>
<li>猎豹浏览器基于 Chromium 112.0.5615.138</li>
<li>Brave 1.86.148 基于 Chromium 144.0.7559.133</li>
</ul>
</li>
</ul>
<div>
本程序目前仅支持 Chromium 内核浏览器因此 SafariFirefoxWindows Internet
Explorer旧版 Microsoft Edge <span color-orange
>非Chromium内核浏览器无法使用</span
>同时对于 Chromium 内核浏览器本程序<span color-orange
>仅支持
{{ EXPECT_CHROMIUM_BUILD_ID }}
或更高内核版本</span
>你可以打开在你想要尝试的浏览器访问 chrome://version” 找到 “用户代理” /
User Agent 行来查看当前浏览器的内核版本<br />
目前大部分中国大陆厂商发布的基于Chromium内核的浏览器均由于版本过低不被本程序支持
</div>
</div>
</details>
<details class="faq-item">
<summary>如果我选择了一个不支持的浏览器会发生什么</summary>
<div class="faq-answer">
<div>可能会发生的情况</div>
<ul>
<li>浏览器将会启动但会开启一个空白页面之后浏览器不会做任何事</li>
<li>浏览器启动失败你不会看到任何界面</li>
<li>浏览器会每隔一段时间打开一个新的浏览器窗口</li>
</ul>
</div>
</details>
<details class="faq-item">
<summary>如果我选择了一个不是浏览器的可执行文件会发生什么</summary>
<div class="faq-answer">
<div>可能会发生的情况</div>
<ul>
<li>可执行文件将会启动但会开启一个空白页面之后可执行文件不会做任何事</li>
<li>可执行文件闪退并报错闪退后会自动重启继续报错</li>
<li>可执行文件会不断运行多个实例</li>
</ul>
</div>
</details>
</div>
</div>
</div>
</el-form>
</div>
<div class="bg-#f8f8f8 pb10px pt10px w-full flex-0">
<div
:style="{
display: 'flex',
justifyContent: 'end',
width: '90%',
margin: '0 auto',
paddingLeft: '',
paddingRight: ''
}"
>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</div>
</div>
</div>
<!-- <div class="h60px mt14px">
<RouterView
class="h100%"
:dependencies-status="checkDependenciesResult"
:process-waitee="downloadProcessWaitee"
></RouterView>
</div> -->
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { nextTick, ref } from 'vue'
import debounce from 'lodash/debounce'
import { ElMessage } from 'element-plus'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../common/constant'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
const { ipcRenderer } = electron
useRouter()
// const checkDependenciesResult = ref({})
// const downloadProcessWaitee = ref(null)
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'cookie-assistant',
...params
})
}
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 }
)
const formData = ref({
browserPath: ''
})
const rules = {
browserPath: {
validator: async (_, value, callback) => {
if (!value?.trim()) {
callback(new Error('请输入浏览器可执行文件路径'))
return
}
const err = await ipcRenderer.invoke('check-executable-file', value)
if (err) {
callback(err?.message ?? '文件无效 - 未知原因')
} else {
callback()
}
},
trigger: 'blur'
}
}
const isAutoDetectLoading = ref(false)
async function autoDetectPuppeteerExecutable() {
isAutoDetectLoading.value = true
await sleep(50)
try {
const result = await ipcRenderer.invoke('get-any-available-puppeteer-executable', {
ignoreCached: true,
noSave: true
})
if (!result) {
ElMessage({
message: '未检测到可用浏览器的可执行文件',
type: 'warning',
grouping: true
})
return
}
formData.value.browserPath = result.executablePath
ElMessage({
message: '已找到可用浏览器,可执行文件路径已填入输入框',
type: 'success',
grouping: true
})
await nextTick()
await formRef.value.validateField()
} finally {
isAutoDetectLoading.value = false
}
}
async function chooseExecutableFile() {
const chooseResult = await ipcRenderer.invoke('choose-file', {
fileChooserConfig: {
properties: ['openFile', 'treatPackageAsDirectory'],
filters: [
{
name: '可执行文件',
extensions: (await ipcRenderer.invoke('get-os-platform')) === 'win32' ? ['exe'] : ['']
},
{ name: '所有文件', extensions: ['*'] }
]
}
})
if (chooseResult.canceled || !chooseResult.filePaths?.length) {
return
}
formData.value.browserPath = chooseResult.filePaths[0]
await nextTick()
await formRef.value.validateField()
}
ipcRenderer.invoke('get-last-used-and-available-browser').then((res) => {
formData.value.browserPath = res?.executablePath ?? ''
})
function handleCancel() {
gtagRenderer('cancel_clicked')
window.close()
}
const formRef = ref()
async function handleSave() {
await formRef.value.validate()
await ipcRenderer.invoke('save-last-used-and-available-browser-info', {
executablePath: formData.value.browserPath,
browser: ''
})
await ipcRenderer.send('browser-config-saved')
}
const handleFeedbackClick = () => {
gtagRenderer('goto_feedback_for_ba_clicked')
electron.ipcRenderer.send('send-feed-back-to-github-issue')
}
const handleClickLaunchBrowserDownloader = async () => {
gtagRenderer('launch_browser_downloader_clicked')
const downloadedBrowserPath = await electron.ipcRenderer.invoke(
'download-browser-with-downloader'
)
if (downloadedBrowserPath) {
formData.value.browserPath = downloadedBrowserPath
ElMessage({
message: '浏览器下载成功,可执行文件路径已填入输入框',
type: 'success',
grouping: true
})
}
}
</script>
<style lang="scss" scoped>
a:link,
a:visited,
a:hover,
a:active {
color: #409eff;
}
.faq-main {
.faq-item {
summary {
padding: 4px 0;
}
.faq-answer {
color: #666;
margin-left: 12px;
ul {
margin-top: 0;
margin-bottom: 0;
padding-left: 1em;
}
}
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="h-screen of-hidden flex flex-col flex-items-center flex-justify-center">
<div>
<div>
由于您是首次使用本程序或者您之前配置的浏览器被卸载/被删除/被移动/被更新/版本太旧因此需要重新配置浏览器
</div>
<div>
首先将尝试自动配置自动配置成功后本对话框将自动关闭如自动配置失败请在下个页面中手动配置
</div>
<div>正在尝试自动配置请稍等...</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
const { ipcRenderer } = electron
const router = useRouter()
// const checkDependenciesResult = ref({})
// const downloadProcessWaitee = ref(null)
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'browser-auto-find',
...params
})
}
async function autoDetectPuppeteerExecutable() {
const result = await ipcRenderer.invoke('get-any-available-puppeteer-executable')
if (!result) {
gtagRenderer('first-run-auto-detect-pptr-exe-fail')
ElMessage({
message: '未找到可用浏览器的可执行文件,请尝试手动配置',
type: 'warning',
grouping: true
})
router.replace({
path: '/browserAssistant',
query: {
firstRun: 1
}
})
return
}
gtagRenderer('first-run-auto-detect-pptr-exe-success')
await ipcRenderer.send('browser-config-saved')
}
autoDetectPuppeteerExecutable()
</script>
<style lang="scss" scoped>
a:link,
a:visited,
a:hover,
a:active {
color: #409eff;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<p>核心组件下载失败请重试</p>
<br />
<p>浏览器下载失败请重试或使用其他方式下载</p>
<!-- <br />
<br />
<p>
<b text-orange>提示</b>由于网络颠簸如果多次重试仍然失败&nbsp;<el-button
@@ -12,23 +12,23 @@
>点击此处</el-button
>&nbsp;下载最新版本 Google Chrome
浏览器安装完毕后重新打开本程序程序会自动检测该浏览器并使用它
</p>
</p> -->
</div>
</template>
<script lang="ts" setup>
import { gtagRenderer } from '@renderer/utils/gtag'
import debounce from 'lodash-es/debounce'
const { ipcRenderer } = electron
// import { gtagRenderer } from '@renderer/utils/gtag'
// import debounce from 'lodash/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 }
)
// 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>

View File

@@ -0,0 +1,92 @@
<template>
<!-- flex-items- -->
<div class="flex flex-col flex-justify-center w500px h-full ml-auto mr-auto">
<div font-size-14px>正在下载 Google Chrome for Testing {{ EXPECT_CHROMIUM_BUILD_ID }}</div>
<el-progress
:percentage="browserDownloadPercentage"
:format="(n) => `${n.toFixed(1)}%`"
:stroke-width="10"
class="w500px"
mt10px
/>
<div mt10px>
<el-button @click="handleCancelDownload">取消下载</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, 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'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../common/constant'
const browserDownloadPercentage = ref(0)
const handleBrowserDownloadProgress = (ev, { downloadedBytes, totalBytes }) => {
browserDownloadPercentage.value = (downloadedBytes / totalBytes) * 100
}
electron.ipcRenderer.on('PUPPETEER_DOWNLOAD_PROGRESS', handleBrowserDownloadProgress)
onUnmounted(() =>
electron.ipcRenderer.removeListener('PUPPETEER_DOWNLOAD_PROGRESS', handleBrowserDownloadProgress)
)
const downloadProcessExitCode = ref(0)
let executablePath
const processDownloadBrowser = async () => {
downloadProcessExitCode.value = 0
browserDownloadPercentage.value = 0
let restRetriedTime = 2
while (restRetriedTime > 0) {
try {
try {
executablePath = 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)
}
}
}
const processTasks = async () => {
try {
await processDownloadBrowser()
electron.ipcRenderer.send('browser-download-done', executablePath)
} catch (err) {
gtagRenderer('encounter_error_when_download_deps')
await ElMessageBox.confirm(h(FailMessage), {
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false,
type: 'error',
cancelButtonText: '取消',
confirmButtonText: '重试'
})
.then(() => {
gtagRenderer('start_retry_download_deps')
processTasks()
})
.catch(() => {
gtagRenderer('cancel_download_deps_from_err_dialog')
window.close()
})
}
}
processTasks()
function handleCancelDownload() {
gtagRenderer('cancel_download_deps_from_cancel_btn')
window.close()
}
</script>

View File

@@ -1,16 +1,11 @@
<template>
<div class="cookie-assistant-page">
<div ml1em mt1em mb1em font-size-16px>Boss 登录助手</div>
<el-alert
v-if="cookieInvalid"
type="warning"
:closable="false"
title="需要获取您的Boss直聘Cookie才能继续"
>
由于您是首次使用本程序或者您之前使用的Boss直聘账号登录状态失效因此您需要重新获取登录凭证
<div ml1em mt1em mb1em font-size-16px>BOSS登录助手</div>
<el-alert v-if="cookieInvalid" type="warning" :closable="false">
由于您是首次使用本程序或者您之前使用的BOSS直聘账号登录状态失效因此您需要重新获取登录凭证
</el-alert>
<div ml1em mt1em line-height-normal>
如果您了解如何获取Cookie了解有效的Cookie格式可以直接在下方输入框中进行编辑由于手动编辑较为麻烦建议您打开已登录过Boss直聘的浏览器使用<a
如果您了解如何获取Cookie了解有效的Cookie格式可以直接在下方输入框中进行编辑由于手动编辑较为麻烦建议您打开已登录过BOSS直聘的浏览器使用<a
class="color-blue! decoration-none"
href="javascript:void(0)"
@click.prevent="handleEditThisCookieExtensionStoreLinkClick"
@@ -29,14 +24,14 @@
>
启动浏览器
</li>
<li>按照正常流程通过 <b>短信验证码/二维码/微信小程序</b> 登录您的Boss直聘账号</li>
<li>按照正常流程通过 <b>短信验证码/二维码/微信小程序</b> 登录您的BOSS直聘账号</li>
<li>接下来将自动进行一些页面跳转最终将会停留在首页</li>
<li>
登录后预计5-10秒内具体取决于您的网速您的Cookie将被自动填入下方输入框
<details>
<summary color-orange cursor-pointer>我已完成登录但Cookie一直没出现</summary>
<div ml-2em max-h-200px of-auto>
如果您确实已经在打开浏览器中看到您已登录了Boss直聘请尝试按照如图所示方式复制Cookie
如果您确实已经在打开浏览器中看到您已登录了BOSS直聘请尝试按照如图所示方式复制Cookie
<figure>
<figcaption>依次点击浏览器右上角扩展程序图标EditThisCookie图标</figcaption>
<img block max-w-full src="./resources/copy-cookie-step-1.png" />
@@ -98,7 +93,7 @@
</el-form-item>
</el-form>
<footer flex mt20px pb20px flex-justify-end>
<el-button v-if="!cookieInvalid" @click="handleCancel">取消</el-button>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</footer>
</div>
@@ -201,7 +196,7 @@ const handleEditThisCookieExtensionStoreLinkClick = () => {
const handleCancel = () => {
gtagRenderer('cancel_clicked')
router.replace('/main-layout')
window.close()
}
const handleSubmit = async () => {
gtagRenderer('save_clicked')
@@ -210,9 +205,10 @@ const handleSubmit = async () => {
fileName: 'boss-cookies.json',
data: formContent.value.collectedCookies
})
ElMessage.success('Boss直聘 Cookie 保存成功')
ElMessage.success('BOSS直聘 Cookie 保存成功')
gtagRenderer('save_cookie_done')
router.replace('/main-layout')
window.electron.ipcRenderer.send('cookie-saved')
}
const handleBossZhipinLoginPageClosed = () => {

View File

@@ -22,8 +22,8 @@
gtagRenderer('view_boss_agreement_clicked')
}
"
>Boss直聘用户协议</el-link
>2023年3月版相关条款相违背您在注册Boss直聘时已签署过这一条款根据该条款
>BOSS直聘用户协议</el-link
>2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款
<i>用户的平台使用义务</i><i>违约责任</i>
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录账号被限制使用账号被封禁</b
@@ -31,10 +31,10 @@
>且如果相关风险发生<b class="color-red">您需要自行承担相关后果本程序不负责</b>
</ElCheckbox>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘<b
本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊Boss以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>也不会进行除自动开聊BOSS以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
@@ -42,11 +42,11 @@
class="color-red"
>注意节制</b
>建议当天开聊次数用尽后隔几天再使用建议您<b class="color-red"
>注册一个本程序专用的新的Boss直聘账号</b
>注册一个本程序专用的新的BOSS直聘账号</b
>进行求职
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
本程序原理是模拟用户在Boss直聘网页上寻找关键元素并进行点击操作Boss直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red"
>浏览器重复闪退重新启动</b
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
@@ -81,16 +81,16 @@
</ElCheckbox>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在Boss直聘上发布职位信息真实性由Boss直聘负责<b
>为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责<b
class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
>
</ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃<b
请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃<b
class="color-red"
>在Boss直聘上屏蔽当前公司及与之关联的公司</b
>在BOSS直聘上屏蔽当前公司及与之关联的公司</b
>
</ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
@@ -129,7 +129,7 @@ const readmeItemCheckStatusList = ref<number[]>([])
const handleCancel = async () => {
gtagRenderer('cancel_clicked')
await sleep(500)
electron.ipcRenderer.invoke('exit-app-immediately')
window.close()
}
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
@@ -150,7 +150,7 @@ const handleSubmit = () => {
}
return
}
electron.ipcRenderer.invoke('first-launch-notice-approve')
electron.ipcRenderer.send('first-launch-notice-approve')
gtagRenderer('submit_done')
}
const handleReadmeItemCheckStatusListChange = (value: number[]) => {

View File

@@ -3,7 +3,7 @@
<FlyingCompanyLogoList class="flying-company-logo-list" />
<div class="tip">
<article>
<h1>👋 BOSS炸弹正在运行</h1>
<h1>👋 自动开聊正在运行</h1>
<p>💬 正在为你开聊BOSS请静候佳音</p>
<p>📱 你可以在<b>手机</b> / <b>平板电脑</b>使用BOSS直聘App与为你开聊的BOSS聊天</p>
<p>🍀 祝你求职顺利</p>

View File

@@ -1,9 +1,9 @@
<template>
<div class="geek-auto-start-chat-with-boss__running-status">
<div class="read-no-reply-auto-reminder__running-status">
<FlyingCompanyLogoList class="flying-company-logo-list" />
<div class="tip">
<article>
<h1>👋 已读不回提醒器正在运行</h1>
<h1>👋 已读不回自动复聊正在运行</h1>
<p>🍀 祝你求职顺利</p>
</article>
<el-button :disabled="isStopping" @click="handleStopButtonClick">停止开聊</el-button>
@@ -23,7 +23,7 @@ const router = useRouter()
const handleStopButtonClick = async () => {
gtagRenderer('rnrr_stop_button_clicked')
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
ipcRenderer.invoke('stop-read-no-reply-auto-reminder')
}
const isStopping = ref(false)
@@ -31,23 +31,24 @@ const handleStopping = () => {
gtagRenderer('rnrr_become_stopping')
isStopping.value = true
}
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
ipcRenderer.once('read-no-reply-auto-reminder-stopping', handleStopping)
const handleStopped = () => {
gtagRenderer('rnrr_become_stopped')
router.replace('/main-layout/ReadNoReplyReminder')
}
ipcRenderer.once('geek-auto-start-chat-with-boss-stopped', handleStopped)
ipcRenderer.once('read-no-reply-auto-reminder-stopped', handleStopped)
onUnmounted(() => {
ipcRenderer.removeListener('geek-auto-start-chat-with-boss-stopped', handleStopped)
ipcRenderer.removeListener('geek-auto-start-chat-with-boss-stopping', handleStopping)
ipcRenderer.removeListener('read-no-reply-auto-reminder-stopped', handleStopped)
ipcRenderer.removeListener('read-no-reply-auto-reminder-stopping', handleStopping)
})
onMounted(async () => {
try {
await electron.ipcRenderer.invoke('run-read-no-reply-auto-reminder')
} catch (err) {
}
catch (err) {
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
gtagRenderer('rnrr_cannot_run_for_corrupt')
ElMessage.error({
@@ -62,7 +63,7 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
.geek-auto-start-chat-with-boss__running-status {
.read-no-reply-auto-reminder__running-status {
width: 100%;
height: 100%;
overflow: hidden;

View File

@@ -1,54 +1,29 @@
<template><RouterView :status="currentStatus" /></template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const currentStatus = ref('')
onMounted(() => {
const promise = electron.ipcRenderer.invoke('prepare-run-geek-auto-start-chat-with-boss')
const handleLocatingPuppeteerExecutable = () => {
currentStatus.value = 'locating-puppeteer-executable'
switch (route.query.flow) {
case 'geek-auto-start-chat-with-boss': {
router.replace({
path: '/geekAutoStartChatWithBoss/runningStatus'
})
break
}
case 'read-no-reply-reminder': {
router.replace({
path: '/geekAutoStartChatWithBoss/runningStatusForReadNoReplyReminder'
})
break
}
default: {
router.replace('/')
}
}
electron.ipcRenderer.once('locating-puppeteer-executable', handleLocatingPuppeteerExecutable)
onUnmounted(() => {
electron.ipcRenderer.removeListener(
'locating-puppeteer-executable',
handleLocatingPuppeteerExecutable
)
})
promise
.then(() => {
switch (route.query.flow) {
case 'geek-auto-start-chat-with-boss': {
router.replace({
path: '/geekAutoStartChatWithBoss/runningStatus'
})
break
}
case 'read-no-reply-reminder': {
router.replace({
path: '/geekAutoStartChatWithBoss/runningStatusForReadNoReplyReminder'
})
break
}
default: {
router.replace('/')
}
}
})
.catch(async (err) => {
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
ElMessage.error({
message: `核心组件损坏,正在尝试修复`
})
router.replace('/')
}
console.error(err)
})
})
</script>

View File

@@ -50,6 +50,10 @@
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
</template>
<template v-if="row.markReason === MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT">
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
</template>
</template>
</ElTableColumn>
<ElTableColumn prop="experienceName" label="工作经验" />
@@ -214,12 +218,13 @@ function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) {
}
const markReasonTopicMap = {
[MarkAsNotSuitReason.BOSS_INACTIVE]: 'Boss不活跃',
[MarkAsNotSuitReason.BOSS_INACTIVE]: 'BOSS不活跃',
[MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON]: '手动标记不合适',
[MarkAsNotSuitReason.JOB_NOT_SUIT]: '职位不合适',
[MarkAsNotSuitReason.JOB_CITY_NOT_SUIT]: '工作地不合适',
[MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT]: '工作经验不合适',
[MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT]: '薪资不合适'
[MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT]: '薪资不合适',
[MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT]: '公司名称不匹配'
}
function formatMarkReason(row: VMarkAsNotSuitLog) {
@@ -233,8 +238,8 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
}
})()
return [
extInfo?.bossActiveTimeDesc && `Boss活跃情况:${extInfo.bossActiveTimeDesc}`,
extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`
extInfo?.bossActiveTimeDesc && `BOSS活跃情况:${extInfo.bossActiveTimeDesc}`,
extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`
]
.filter(Boolean)
.join('\n')
@@ -247,7 +252,7 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
return null
}
})()
return [extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`]
return [extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`]
.filter(Boolean)
.join('\n')
}
@@ -260,7 +265,7 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
return null
}
})()
return [extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`]
return [extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`]
.filter(Boolean)
.join('\n')
}
@@ -274,11 +279,23 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
})()
return [
extInfo?.salaryDesc && `薪资:${extInfo.salaryDesc}`,
extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`
extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`
]
.filter(Boolean)
.join('\n')
}
case MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT: {
const extInfo = (() => {
try {
return JSON.parse(row.extInfo)
} catch {
return null
}
})()
return [extInfo?.chosenReasonInUi?.text && `BOSS选项内容${extInfo.chosenReasonInUi.text}`]
.filter(Boolean)
.join('\n')
}
default: {
return ''
}

View File

@@ -1,225 +1,248 @@
<template>
<div class="form-wrap">
<el-form
ref="formRef"
:rules="formRules"
:model="formContent.autoReminder"
label-position="top"
>
<el-form-item label="BOSS直聘 Cookie">
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
>编辑Cookie</el-button
>
</el-form-item>
<el-form-item>
<div>
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
发送提醒消息前先按照Boss炸弹-职位类型正则校验正在与Boss沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<template v-else>
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithExpectJobType">
发送提醒消息前先按照Boss炸弹-职位类型正则校验正在与Boss沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前职位类型正则{{ expectJobTypeRegExpStr?.trim() }}</div>
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
<div>当前简历中填写的期望职位:{{ resumeContent?.expectJob ?? '-' }}</div>
<div color-orange>请确保上方二者信息匹配</div>
</template>
</div>
</template>
</div>
</el-form-item>
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的Boss时将要向Boss发出">
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
<div class="read-no-reply-reminder__wrap">
<div class="form-wrap">
<el-form
ref="formRef"
:rules="formRules"
:model="formContent.autoReminder"
label-position="top"
>
<el-form-item>
<div>
<el-tooltip
effect="light"
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_about_lfr_emotion_figure')"
>
<template #content>
<img block h-100px src="./resources/look-forward-reply-emotion.gif" />
</template>
<el-radio :label="RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION">
“[盼回复]” 表情
</el-radio>
</el-tooltip>
<br />
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
</div>
</el-radio-group>
</el-form-item>
<div class="ml-30px">
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickConfigLlm">
配置大语言模型
</el-button>
<div class="font-size-12px color-#666">
支持
<span
class="pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #3c4efd"
>DeepSeek-V3</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #000000"
>GPT-4o mini</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #462ac4"
>Qwen2.5</span
>
模型;支持多个“服务商-模型”组合按权重搭配使用
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<div>
<el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
gtagRenderer('reset_template_clicked_in_main_form')
restoreDefaultTemplate()
}
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
发送提醒消息前先按照自动开聊-职位类型正则校验正在与BOSS沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<template v-else>
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithExpectJobType">
发送提醒消息前先按照自动开聊-职位类型正则校验正在与BOSS沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前职位类型正则{{ expectJobTypeRegExpStr?.trim() }}</div>
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
还原默认提示词模板
</el-button>
<div>当前简历中填写的期望职位:{{ resumeContent?.expectJob ?? '-' }}</div>
<div color-orange>请确保上方二者信息匹配</div>
</template>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</template>
</div>
</el-form-item>
<el-form-item>
<div>
<el-checkbox v-if="!blockCompanyNameRegExpStr?.trim()" :model-value="false" disabled>
发送提醒消息前,先按照“自动开聊-不期望投递公司正则”校验正在与BOSS沟通的岗位是否归属于不期望投递的公司如果是则不提醒
</el-checkbox>
<template v-else>
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithoutBlockCompanyName">
发送提醒消息前,先按照“自动开聊-不期望投递公司正则”校验正在与BOSS沟通的岗位是否归属于不期望投递的公司如果是则不提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前不期望投递公司正则:{{ blockCompanyNameRegExpStr?.trim() }}</div>
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
</template>
</div>
</el-form-item>
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的BOSS时将要向BOSS发出">
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回复聊过程</el-button
>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div class="flex flex-items-center">
<span class="whitespace-nowrap">当所有模型均不可使用时&nbsp;</span>
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
<el-tooltip
effect="light"
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_about_lfr_emotion_figure')"
>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
<template #content>
<img block h-100px src="./resources/look-forward-reply-emotion.gif" />
</template>
<el-radio :label="RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION">
“[盼回复]” 表情
</el-radio>
</el-tooltip>
<br />
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
</div>
</el-form-item>
</template>
</div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
<el-input-number
v-model="formContent.autoReminder.throttleIntervalMinutes"
class="w-150px"
:min="3"
:precision="1"
:step="0.5"
@blur="handleThrottleIntervalMinutesBlur"
/>&nbsp;分钟内不多次跟进同一Boss
</el-form-item>
<el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
<div>
<div><el-checkbox v-model="enableRechatLimit" />&nbsp;启用</div>
</el-radio-group>
</el-form-item>
<div class="ml-30px">
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<div>
<el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
gtagRenderer('reset_template_clicked_in_main_form')
restoreDefaultTemplate()
}
"
>
还原默认提示词模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回自动复聊过程</el-button
>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div class="flex flex-items-center">
<span class="whitespace-nowrap">当所有模型均不可使用时&nbsp;</span>
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
</div>
</el-form-item>
</template>
</div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
<el-input-number
v-model="formContent.autoReminder.rechatLimitDay"
v-model="formContent.autoReminder.throttleIntervalMinutes"
class="w-150px"
:min="0"
:min="3"
:precision="1"
:step="0.5"
:disabled="!enableRechatLimit"
/>&nbsp;天<br />
<div v-if="enableRechatLimit">
不再跟进&nbsp;<span class="text-orange">{{ rechatLimitDateString }}</span
>)之前列表中没有进展的聊天
@blur="handleThrottleIntervalMinutesBlur"
/>&nbsp;分钟内不多次跟进同一BOSS
</el-form-item>
<el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
<div>
<div><el-checkbox v-model="enableRechatLimit" />&nbsp;启用</div>
<el-input-number
v-model="formContent.autoReminder.rechatLimitDay"
class="w-150px"
:min="0"
:precision="1"
:step="0.5"
:disabled="!enableRechatLimit"
/>&nbsp;天<br />
<div v-if="enableRechatLimit">
不再跟进&nbsp;<span class="text-orange">{{ rechatLimitDateString }}</span
>)之前列表中没有进展的聊天
</div>
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span></div>
</div>
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span></div>
</div>
</el-form-item>
<el-form-item>
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_stop_trace_one_boss')"
>
<template #content>
<ul m0 line-height-1.5em w-300px pl2em>
<li>
请向你不想继续提醒的Boss发送任意消息发送后立即撤回的这条消息即可。
<br />
<br />
对于PC端Boss直聘鼠标移动到要撤回的消息点按鼠标右键调出菜单再鼠标左键点击菜单中的“撤回”。如图所示
<br />
<img block w-full src="./resources/withdraw-message-guide.png" />
</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>我不想持续提醒某个Boss了如何处理</el-button
</el-form-item>
<el-form-item>
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_stop_trace_one_boss')"
>
</el-tooltip>
</el-form-item>
<el-form-item class="last-form-item">
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
</el-form-item>
</el-form>
<template #content>
<ul m0 line-height-1.5em w-300px pl2em>
<li>
请向你不想继续提醒的Boss发送任意消息发送后立即撤回的这条消息即可。
<br />
<br />
对于PC端BOSS直聘鼠标移动到要撤回的消息点按鼠标右键调出菜单再鼠标左键点击菜单中的“撤回”。如图所示
<br />
<img block w-full src="./resources/withdraw-message-guide.png" />
</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>我不想持续提醒某个BOSS了如何处理</el-button
>
</el-tooltip>
</el-form-item>
<el-form-item class="last-form-item" flex>
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
</el-form-item>
</el-form>
</div>
<div
class="runing-overlay__wrap"
:style="{
pointerEvents: 'none'
}"
>
<RuningOverlay
ref="runningOverlayRef"
worker-id="readNoReplyAutoReminderMain"
:run-record-id="runRecordId"
>
<template #op-buttons="{ currentRunningStatus }">
<div>
<template v-if="currentRunningStatus === RUNNING_STATUS_ENUM.RUNNING">
<el-button
type="danger"
plain
:loading="isStopButtonLoading"
@click="handleStopButtonClick"
>结束任务</el-button
>
</template>
<template v-else>
<el-button
type="primary"
@click="
() => {
runningOverlayRef?.hide?.()
}
"
>关闭</el-button
>
</template>
</div>
</template>
</RuningOverlay>
</div>
</div>
</template>
@@ -229,11 +252,13 @@ import { dayjs, ElForm, ElMessage, ElMessageBox, ElSelect, ElOption } from 'elem
import { useRouter } from 'vue-router'
import {
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK
RECHAT_LLM_FALLBACK,
RUNNING_STATUS_ENUM
} from '../../../../common/enums/auto-start-chat'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import mittBus from '../../utils/mitt'
import { QuestionFilled } from '@element-plus/icons-vue'
import RuningOverlay from '@renderer/features/RunningOverlay/index.vue'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'rnrr-config',
@@ -248,7 +273,8 @@ const formContent = ref({
rechatContentSource: 1,
recentMessageQuantityForLlm: 8,
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION,
onlyRemindBossWithExpectJobType: true
onlyRemindBossWithExpectJobType: true,
onlyRemindBossWithoutBlockCompanyName: true
}
})
@@ -286,15 +312,17 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
})
const expectJobTypeRegExpStr = ref('')
async function fetchExpectJobTypeRegExpStr() {
const blockCompanyNameRegExpStr = ref('')
async function fetchAutoStartChatConfig() {
await electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
expectJobTypeRegExpStr.value = res.config['boss.json']?.expectJobTypeRegExpStr
blockCompanyNameRegExpStr.value = res.config['boss.json']?.blockCompanyNameRegExpStr
})
}
fetchExpectJobTypeRegExpStr()
mittBus.on('auto-start-chat-with-boss-config-saved', fetchExpectJobTypeRegExpStr)
fetchAutoStartChatConfig()
mittBus.on('auto-start-chat-with-boss-config-saved', fetchAutoStartChatConfig)
onUnmounted(() => {
mittBus.off('auto-start-chat-with-boss-config-saved', fetchExpectJobTypeRegExpStr)
mittBus.off('auto-start-chat-with-boss-config-saved', fetchAutoStartChatConfig)
})
const resumeContent = ref(null)
@@ -434,7 +462,8 @@ async function checkIsCanRun() {
return true
}
const runRecordId = ref(null)
const runningOverlayRef = ref(null)
const handleSubmit = async () => {
gtagRenderer('run_read_no_reply_reminder_clicked', {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
@@ -471,10 +500,29 @@ const handleSubmit = async () => {
}
}
gtagRenderer('run_read_no_reply_reminder_launched')
router.replace({
path: '/geekAutoStartChatWithBoss/prepareRun',
query: { flow: 'read-no-reply-reminder' }
})
try {
runningOverlayRef.value?.show()
const { runRecordId: rrId } = await electron.ipcRenderer.invoke(
'run-read-no-reply-auto-reminder'
)
runRecordId.value = rrId
} catch (err) {
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
gtagRenderer('rnrr_cannot_run_for_corrupt')
ElMessage.error({
message: `核心组件损坏,正在尝试修复`
})
router.replace('/')
}
console.error(err)
gtagRenderer('rnrr_cannot_run_for_unknown_error', { err })
}
// {
// path: '/geekAutoStartChatWithBoss/prepareRun',
// query: { flow: 'read-no-reply-reminder' }
// }
}
function handleThrottleIntervalMinutesBlur() {
if (formContent.value.autoReminder.throttleIntervalMinutes < 3) {
@@ -493,11 +541,6 @@ const restoreDefaultTemplate = async () => {
})
}
const handleClickLaunchLogin = () => {
gtagRenderer('launch_login_clicked')
router.replace('/cookieAssistant')
}
const currentStamp = ref(new Date())
let timer = 0
function updateCurrentStamp() {
@@ -515,15 +558,6 @@ const rechatLimitDateString = computed(() => {
).format('YYYY-MM-DD HH:mm:ss')
})
const handleClickConfigLlm = async () => {
gtagRenderer('config_llm_clicked')
try {
await electron.ipcRenderer.invoke('llm-config')
} catch (err) {
console.log(err)
}
}
const handleClickEditResume = async () => {
gtagRenderer('edit_resume_clicked')
try {
@@ -545,7 +579,7 @@ const rechatLlmFallbackOptions = [
value: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
},
{
name: '退出已读不回提醒器',
name: '退出已读不回自动复聊',
value: RECHAT_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
}
]
@@ -559,24 +593,54 @@ async function handleTestEffectClicked() {
autoReminderConfig: JSON.parse(JSON.stringify(formContent.value.autoReminder))
})
}
const needToCheckRuntimeDependenciesHandler = () => {
router.replace('/')
}
electron.ipcRenderer.on('need-to-check-runtime-dependencies', needToCheckRuntimeDependenciesHandler)
onUnmounted(() => {
electron.ipcRenderer.removeListener(
'need-to-check-runtime-dependencies',
needToCheckRuntimeDependenciesHandler
)
})
const isStopButtonLoading = ref(false)
const handleStopButtonClick = async () => {
gtagRenderer('rnrr_stop_button_clicked')
isStopButtonLoading.value = true
try {
electron.ipcRenderer.invoke('stop-read-no-reply-auto-reminder')
runningOverlayRef.value?.hide()
} finally {
isStopButtonLoading.value = false
}
}
</script>
<style scoped lang="scss">
.form-wrap {
max-height: 100vh;
overflow: auto;
padding-left: 20px;
padding-right: 20px;
:deep(.el-form) {
margin: 0 auto;
max-width: 1000px;
padding-top: 8px;
}
.last-form-item {
:deep(.el-form-item__content) {
margin-top: 0px;
justify-content: flex-end;
<style lang="scss">
.read-no-reply-reminder__wrap {
position: relative;
.form-wrap {
max-height: 100vh;
overflow: auto;
padding-left: 20px;
padding-right: 20px;
.el-form {
margin: 0 auto;
max-width: 1000px;
padding-top: 8px;
}
.last-form-item {
.el-form-item__content {
margin-top: 0px;
justify-content: flex-end;
}
}
}
.runing-overlay__wrap {
position: absolute;
inset: 0;
}
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<TaskManagerList />
</template>
<script lang="ts" setup>
import TaskManagerList from '../../features/TaskManager/List.vue'
</script>

View File

@@ -1,99 +1,155 @@
<template>
<div class="flex h100vh">
<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炸弹
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
>
<template #content>
<div w-480px>
<div>扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的Boss</div>
<br />
<div>匹配步骤</div>
<ol m0 pl2em>
<li>
按照公司名称查找职位查找到目标职位后自动点击这个职位右侧将会展示职位详情
</li>
<li>
检查Boss活跃度
<ul pl2em>
<div class="nav-list flex-1 of-auto pl20px ml--20px">
<RouterLink v-show="false" to="./TaskManager">任务管理</RouterLink>
<div class="group-item">
<div class="group-title">BOSS直聘</div>
<div flex flex-col class="link-list">
<RouterLink to="./GeekAutoStartChatWithBoss">
自动开聊
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
>
<template #content>
<div w-480px>
<div>扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的BOSS</div>
<br />
<div>匹配步骤</div>
<ol m0 pl2em>
<li>
如果Boss活跃度为本月活跃或更往前的时间则会把职位标记为不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位
按照公司名称查找职位查找到目标职位后自动点击这个职位右侧将会展示职位详情
</li>
<li>
检查BOSS活跃度
<ul pl2em>
<li>
如果BOSS活跃度为本月活跃或更往前的时间则会把职位标记为不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位
</li>
</ul>
</li>
<li>
对职位名称职位类型职位描述进行匹配
<ul pl2em>
<li>如果匹配则自动点击开聊按钮</li>
<li>
不匹配则标记这个职位为不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位
</li>
</ul>
</li>
</ol>
<br />
<div>异常情况</div>
<ol m0 pl2em>
<li>
当前页面筛选条件下如果没有更多职位则自动切换备选筛选条件以获取更多新职位
</li>
<li>
如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
</li>
</ol>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<RouterLink to="./ReadNoReplyReminder">
已读不回自动复聊
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div w-480px>
<div>
BOSS不明原因已读不回简历就是投不出去<br />
已读不回自动复聊提醒一下已读不回的 BOSS助力把握每次机会
</div>
<br />
<div>匹配逻辑</div>
<div>在聊天列表中查找对你消息已读不回的BOSS再发一条消息多次复聊同时</div>
<ul m0 pl2em>
<li>如果设置了跟进时限那么在这个时间之前活跃的聊天将不会被检查</li>
<li>
如果设置了跟进间隔且再次检查时发现BOSS已读不回且距离上次提醒时间间隔小于这个时间那么聊天将暂时不会跟进直到下次检查时距离上次提醒时间间隔大于这个时间
</li>
</ul>
</li>
<li>
对职位名称职位类型职位描述进行匹配
<ul pl2em>
<li>如果匹配则自动点击开聊按钮</li>
<li>
不匹配则标记这个职位为不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位
</li>
<br />
<div>发送内容</div>
<ul m0 pl2em>
<li>[盼回复]表情</li>
<li>由大语言模型根据简历及当前聊天上下文生成的内容</li>
</ul>
</li>
</ol>
<br />
<div>异常情况</div>
<ol m0 pl2em>
<li>
当前页面筛选条件下如果没有更多职位则自动切换备选筛选条件以获取更多新职位
</li>
<li>
如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
</li>
</ol>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<a href="javascript:void(0)" @click="handleClickLaunchBossLogin">
编辑登录凭据<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleLaunchBossSite">
手动逛<TopRight w-1em h-1em mr10px />
</a>
</div>
</div>
<hr class="group-divider" />
<div class="group-item">
<div class="group-title">全局设置</div>
<div flex flex-col class="link-list">
<a href="javascript:void(0)" @click="handleClickBrowserSetting">
编辑浏览器偏好<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleClickConfigLlm">
配置大语言模型
<div>
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div class="font-size-12px">
支持
<span
class="pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #3c4efd"
>DeepSeek-V3</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-black border-rd-full font-size-0.8em"
style="background-color: #fff"
>GPT-4o mini</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #462ac4"
>Qwen2.5</span
>
模型<br />支持多个服务商-模型组合按权重搭配使用
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
<TopRight w-1em h-1em mr10px />
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<RouterLink to="./ReadNoReplyReminder">
已读不回提醒器
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div w-480px>
<div>
Boss不明原因已读不回简历就是投不出去<br />
已读不回提醒器有事没事提醒一下已读不回的 Ta助力把握每次机会
</div>
<br />
<div>匹配逻辑</div>
<div>在聊天列表中查找对你消息已读不回的Boss再发一条消息多次复聊同时</div>
<ul m0 pl2em>
<li>如果设置了跟进时限那么在这个时间之前活跃的聊天将不会被检查</li>
<li>
如果设置了跟进间隔且再次检查时发现Boss已读不回且距离上次提醒时间间隔小于这个时间那么聊天将暂时不会跟进直到下次检查时距离上次提醒时间间隔大于这个时间
</li>
</ul>
<br />
<div>发送内容</div>
<ul m0 pl2em>
<li>[盼回复]表情</li>
<li>由大语言模型根据简历及当前聊天上下文生成的内容</li>
</ul>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<hr w180px />
<a href="javascript:void(0)" @click="handleLaunchBossSite">
手动逛Boss<TopRight w-1em h-1em mr10px />
</a>
<hr w180px />
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
<RouterLink to="./JobLibrary">职位库</RouterLink>
<RouterLink to="./BossLibrary">Boss库</RouterLink>
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
</a>
</div>
</div>
<hr class="group-divider" />
<div class="group-item">
<div class="group-title">运行数据</div>
<div flex flex-col class="link-list">
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
<RouterLink to="./JobLibrary">职位库</RouterLink>
<RouterLink to="./BossLibrary">BOSS库</RouterLink>
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
</div>
</div>
</div>
<div class="pt-16px pb-16px flex-0 font-size-12px">
<div v-if="updateStore.availableNewRelease" mb16px>
@@ -136,56 +192,26 @@
</div>
</div>
</div>
<RouterView v-slot="{ Component }" class="flex-1">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
<div class="router-view-wrap">
<RouterView v-slot="{ Component }" class="flex-1 of-hidden">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
import useBuildInfo from '@renderer/hooks/useBuildInfo'
import { debounce } from 'lodash-es'
import { debounce } from 'lodash'
import { gtagRenderer } from '@renderer/utils/gtag'
import { useUpdateStore } from '../../store/index'
import { useUpdateStore, useTaskManagerStore } from '../../store/index'
import { ElMessage } from 'element-plus'
const router = useRouter()
const unmountedCbs: Array<InstanceType<typeof Function>> = []
onUnmounted(() => {
while (unmountedCbs.length) {
const fn = unmountedCbs.shift()!
try {
fn()
} catch {}
}
})
const goToCheckBossZhipinCookieFile = () => router.replace('/cookieAssistant')
onMounted(() => {
electron.ipcRenderer.on('check-boss-zhipin-cookie-file', goToCheckBossZhipinCookieFile)
})
onUnmounted(() => {
electron.ipcRenderer.removeListener(
'check-boss-zhipin-cookie-file',
goToCheckBossZhipinCookieFile
)
})
;(async () => {
const checkDependenciesResult = await electron.ipcRenderer.invoke('check-dependencies')
if (Object.values(checkDependenciesResult).includes(false)) {
router.replace('/')
return
}
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
if (!isCookieFileValid) {
router.replace('/cookieAssistant')
return
}
})()
useRouter()
const { buildInfo } = useBuildInfo()
const handleFeedbackClick = () => {
@@ -217,34 +243,85 @@ function handleViewNewReleaseClick() {
gtagRenderer('click_view_release_form_nav')
electron.ipcRenderer.send('open-external-link', updateStore.availableNewRelease!.releasePageUrl)
}
const taskManagerStore = useTaskManagerStore()
void taskManagerStore
const handleClickLaunchBossLogin = async () => {
gtagRenderer('launch_login_clicked')
try {
await electron.ipcRenderer.invoke('login-with-cookie-assistant')
ElMessage({
type: 'success',
message: '登录凭据保存成功'
})
} catch {
//
}
}
const handleClickBrowserSetting = async () => {
gtagRenderer('browser_setting_clicked')
try {
await electron.ipcRenderer.invoke('config-with-browser-assistant')
ElMessage({
type: 'success',
message: '浏览器偏好保存成功'
})
} catch {
//
}
}
const handleClickConfigLlm = async () => {
gtagRenderer('config_llm_clicked')
try {
await electron.ipcRenderer.invoke('llm-config')
} catch (err) {
console.log(err)
}
}
</script>
<style lang="scss" scoped>
.aside-nav {
background-image: linear-gradient(45deg, #eaf4f1, #dcf6f2);
.nav-list {
> a {
display: flex;
align-items: center;
justify-content: space-between;
height: 2.5em;
box-sizing: border-box;
padding-left: 2em;
&.router-link-active {
background-color: #fff;
font-weight: 700;
color: #2faa9e;
border-radius: 9999px 0 0 9999px;
}
}
> hr {
hr.group-divider {
width: 100%;
border: 0 solid;
height: 1px;
background-color: #b3c8c3;
margin-top: 0;
margin-bottom: 0;
margin-top: 4px;
margin-bottom: 4px;
margin-right: 0;
}
.group-item {
.group-title {
color: #849492;
font-size: 12px;
padding: 0.25em 0;
}
.link-list {
a {
display: flex;
align-items: center;
justify-content: space-between;
height: 2em;
box-sizing: border-box;
padding-left: 1em;
font-size: 14px;
&.router-link-active {
background-color: #fff;
font-weight: 700;
color: #2faa9e;
border-radius: 9999px 0 0 9999px;
position: relative;
box-shadow: 0px 0px 10px rgba(50, 114, 108, 0.187);
}
}
}
}
}
.feedback-button-area,
.update-button-area {
@@ -255,4 +332,10 @@ function handleViewNewReleaseClick() {
}
}
}
.router-view-wrap {
display: flex;
flex: 1;
height: 100%;
box-shadow: -4px 1px 20px rgb(50 114 108 / 29%);
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

After

Width:  |  Height:  |  Size: 232 KiB

View File

@@ -14,7 +14,28 @@ const routes: Array<RouteRecordRaw> = [
path: '/cookieAssistant',
component: () => import('@renderer/page/CookieAssistant/index.vue'),
meta: {
title: 'Boss 登录助手'
title: 'BOSS登录助手'
}
},
{
path: '/browserAssistant',
component: () => import('@renderer/page/BrowserAssistant/index.vue'),
meta: {
title: '浏览器助手'
}
},
{
path: '/browserAutoFind',
component: () => import('@renderer/page/BrowserAutoFind/index.vue'),
meta: {
title: '浏览器助手 - 自动查找浏览器'
}
},
{
path: '/browserDownloadProgress',
component: () => import('@renderer/page/BrowserDownloadProgress/index.vue'),
meta: {
title: '正在下载浏览器'
}
},
{
@@ -35,7 +56,7 @@ const routes: Array<RouteRecordRaw> = [
path: '/readNoReplyReminderLlmMock',
component: () => import('@renderer/page/ReadNoReplyReminderLlmMock/index.vue'),
meta: {
title: '已读不回提醒器 大语言模型测试'
title: '已读不回自动复聊 大语言模型测试'
}
},
{
@@ -43,18 +64,25 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@renderer/page/MainLayout/index.vue'),
redirect: '/main-layout/GeekAutoStartChatWithBoss',
children: [
{
path: 'taskManager',
component: () => import('@renderer/page/MainLayout/TaskManager.vue'),
meta: {
title: '任务管理'
}
},
{
path: 'GeekAutoStartChatWithBoss',
component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss/index.vue'),
meta: {
title: 'BOSS炸弹'
title: '自动开聊'
}
},
{
path: 'ReadNoReplyReminder',
component: () => import('@renderer/page/MainLayout/ReadNoReplyReminder.vue'),
meta: {
title: '已读不回提醒器'
title: '已读不回自动复聊'
}
},
{
@@ -82,7 +110,7 @@ const routes: Array<RouteRecordRaw> = [
path: 'BossLibrary',
component: () => import('@renderer/page/MainLayout/BossLibrary.vue'),
meta: {
title: 'Boss库'
title: 'BOSS库'
}
},
{
@@ -102,14 +130,14 @@ const routes: Array<RouteRecordRaw> = [
path: 'prepareRun',
component: () => import('@renderer/page/GeekAutoStartChatWithBoss/PrepareRun.vue'),
meta: {
title: 'BOSS炸弹 正在预热'
title: '自动开聊 正在预热'
}
},
{
path: 'runningStatus',
component: () => import('@renderer/page/GeekAutoStartChatWithBoss/RunningStatus.vue'),
meta: {
title: 'BOSS炸弹 正在为你开聊BOSS'
title: '自动开聊 正在为你开聊BOSS'
}
},
{
@@ -119,7 +147,7 @@ const routes: Array<RouteRecordRaw> = [
'@renderer/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue'
),
meta: {
title: '已读不回提醒器 正在为你开聊BOSS'
title: '已读不回自动复聊 正在为你开聊BOSS'
}
}
]
@@ -129,16 +157,7 @@ const routes: Array<RouteRecordRaw> = [
component: BootstrapSplash,
meta: {
title: '薪想事成'
},
children: [
{
path: '/downloadingDependencies',
component: () => import('@renderer/page/BootstrapSplash/page/DownloadingDependencies.vue'),
meta: {
title: '正在下载核心组件'
}
}
]
}
}
]

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { NewReleaseInfo } from '../../../common/types/update'
import { ref } from 'vue'
import { throttle } from 'lodash'
export const useUpdateStore = defineStore('update', () => {
const availableNewRelease = ref<NewReleaseInfo | null>(null)
@@ -18,3 +19,16 @@ export const useUpdateStore = defineStore('update', () => {
setInterval(checkUpdate, 30 * 30 * 1000)
return { availableNewRelease }
})
export const useTaskManagerStore = defineStore('taskManager', () => {
const runningTasks = ref<unknown[]>([])
function getRunningTasks() {
const { ipcRenderer } = electron
ipcRenderer.invoke('get-task-manager-list').then(res => {
runningTasks.value = res.workers ?? []
})
}
const throttledGetRunningTasks = throttle(getRunningTasks, 2000)
setInterval(throttledGetRunningTasks, 2 * 1000)
return { runningTasks, getRunningTasks: throttledGetRunningTasks }
})

View File

@@ -0,0 +1,39 @@
diff --git a/index.js b/index.js
index 746525753e19878c0d4711e023e02a77eb5ecf8a..a5c323eb19311235df43c5ae9e0025b7bd1da25d 100644
--- a/index.js
+++ b/index.js
@@ -21,6 +21,7 @@ const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin')
* )
* const browser = await puppeteer.launch()
*/
+
class Plugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts)
@@ -38,7 +39,7 @@ class Plugin extends PuppeteerExtraPlugin {
}
}
- async onPageCreated(page) {
+ async handler (page) {
let ua = await page.browser().userAgent()
if (this.opts.stripHeadless) {
ua = ua.replace('HeadlessChrome/', 'Chrome/')
@@ -52,6 +53,17 @@ class Plugin extends PuppeteerExtraPlugin {
this.debug('new ua', ua)
await page.setUserAgent(ua)
}
+
+ async onBrowser(browser) {
+ const pages = await browser.pages()
+ for (const page of pages) {
+ await this.handler(page)
+ }
+ }
+
+ async onPageCreated(page) {
+ await this.handler(page)
+ }
}
module.exports = function(pluginConfig) {

1125
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff