mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-09 09:27:07 +08:00
Merge branch 'feature/multi-process' into feature/ui
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**->维护者正在疯狂求职中<-**
|
**->维护者正在疯狂求职中<-**
|
||||||
|
|
||||||
一款可以帮助你在Boss直聘上**自动批量开聊Boss**的脚本,基于Puppeteer。
|
一款可以帮助你在BOSS直聘上**自动批量开聊BOSS**的脚本,基于Puppeteer。
|
||||||
|
|
||||||
与每一位牛人站在一起
|
与每一位牛人站在一起
|
||||||
|
|
||||||
@@ -11,10 +11,13 @@
|
|||||||
|
|
||||||
各行各业,无论你是小白还是大佬,都能通过几步简单的配置,快速开始求职!
|
各行各业,无论你是小白还是大佬,都能通过几步简单的配置,快速开始求职!
|
||||||
|
|
||||||
|
## TO 有求职之外其他目的朋友
|
||||||
|
本程序的目标,是帮助求职者尽快寻得一个能让他为之**挥洒汗水**、**努力拼搏**的**事业**。如果有其它目的,请勿下载使用。
|
||||||
|
|
||||||
## 程序有哪些功能?运行逻辑是什么?怎样使用?
|
## 程序有哪些功能?运行逻辑是什么?怎样使用?
|
||||||
|
|
||||||
### Boss炸弹
|
### 自动开聊
|
||||||
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
|
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的BOSS。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
|
||||||
|
|
||||||
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
|
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
|
||||||
|
|
||||||
@@ -24,10 +27,10 @@
|
|||||||
<img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
|
<img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
|
||||||
|
|
||||||
2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
||||||
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、Boss活跃度进行匹配
|
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、BOSS活跃度进行匹配
|
||||||
- 如果可以匹配,则自动点击开聊按钮
|
- 如果可以匹配,则自动点击开聊按钮
|
||||||
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
|
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
|
||||||
- 首先查看是否命中策略为“在Boss直聘上标记不合适”的配置,如果命中则在Boss直聘上点击相关UI,标记不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位(对于“推荐列表中的职位”、“通过搜索找到的职位”,此项会回落到“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”)
|
- 首先查看是否命中策略为“在BOSS直聘上标记不合适”的配置,如果命中则在BOSS直聘上点击相关UI,标记不合适,一段时间内你将不会在BOSS上看到这个职位,且将会推荐新职位置换这个职位(对于“推荐列表中的职位”、“通过搜索找到的职位”,此项会回落到“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”)
|
||||||
- 然后查看是否命中策略为“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”的配置,如果命中则仅在本地数据库中记录,且本次运行将无视这个职位
|
- 然后查看是否命中策略为“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”的配置,如果命中则仅在本地数据库中记录,且本次运行将无视这个职位
|
||||||
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
|
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
|
||||||
|
|
||||||
@@ -37,23 +40,23 @@
|
|||||||
|
|
||||||
#### 运行效果
|
#### 运行效果
|
||||||
|
|
||||||
> Boss直聘界面已改版,如下是旧版Boss直聘界面
|
> BOSS直聘界面已改版,如下是旧版BOSS直聘界面
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
|
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" />
|
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/51a43b0f-118d-4b0a-957c-40a5367e29e4" />
|
||||||
|
|
||||||
|
|
||||||
#### 匹配逻辑
|
#### 匹配逻辑
|
||||||
1. 在聊天列表中查找对你消息已读不回的Boss,再发一条消息,多次复聊;同时:
|
1. 在聊天列表中查找对你消息已读不回的BOSS,再发一条消息,多次复聊;同时:
|
||||||
- 如果设置了“跟进时限”,那么在这个时间之前活跃的聊天将不会被检查
|
- 如果设置了“跟进时限”,那么在这个时间之前活跃的聊天将不会被检查
|
||||||
- 如果设置了“跟进间隔”,且再次检查时发现Boss已读不回,且距离上次提醒时间间隔小于这个时间,那么聊天将暂时不会跟进,直到下次检查时距离上次提醒时间间隔大于这个时间
|
- 如果设置了“跟进间隔”,且再次检查时发现BOSS已读不回,且距离上次提醒时间间隔小于这个时间,那么聊天将暂时不会跟进,直到下次检查时距离上次提醒时间间隔大于这个时间
|
||||||
|
|
||||||
#### 发送内容
|
#### 发送内容
|
||||||
以下二者之一:
|
以下二者之一:
|
||||||
@@ -61,8 +64,8 @@ Boss不明原因已读不回?简历就是投不出去?
|
|||||||
2. 由大语言模型(根据简历及当前聊天上下文)生成的内容
|
2. 由大语言模型(根据简历及当前聊天上下文)生成的内容
|
||||||
|
|
||||||
## 辅助功能介绍
|
## 辅助功能介绍
|
||||||
### Boss 登录助手
|
### BOSS 登录助手
|
||||||
帮你用十分简单的方式,登录到 Boss 直聘,以进行自动开聊、已读不回提醒,避免和乱七八糟的技术名词(类似:Cookie、登录凭证、JSON……)打交道
|
帮你用十分简单的方式,登录到 BOSS 直聘,以进行自动开聊、已读不回提醒,避免和乱七八糟的技术名词(类似:Cookie、登录凭证、JSON……)打交道
|
||||||
|
|
||||||
<img width="1680" alt="image" src="https://github.com/user-attachments/assets/b5d02385-f1c0-4045-b01a-66c5f747fec5" />
|
<img width="1680" alt="image" src="https://github.com/user-attachments/assets/b5d02385-f1c0-4045-b01a-66c5f747fec5" />
|
||||||
|
|
||||||
@@ -73,20 +76,20 @@ Boss不明原因已读不回?简历就是投不出去?
|
|||||||
<img width="576" alt="image" src="https://github.com/user-attachments/assets/ef91642f-b0b0-438f-8c66-e650f0de139b" />
|
<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="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" />
|
<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" />
|
<img width="576" alt="image" src="https://github.com/user-attachments/assets/2cccfece-17e7-4cc2-bf33-67f0ce500625" />
|
||||||
|
|
||||||
@@ -135,25 +138,25 @@ Boss不明原因已读不回?简历就是投不出去?
|
|||||||
在此先谢过了~
|
在此先谢过了~
|
||||||
|
|
||||||
## 为什么要写这个程序?
|
## 为什么要写这个程序?
|
||||||
根据日常使用求职平台(Boss直聘)的经验,在已经开聊很多职位的情况下,经常会推荐:
|
根据日常使用求职平台(BOSS直聘)的经验,在已经开聊很多职位的情况下,经常会推荐:
|
||||||
1. 一些长时间不活跃的“僵尸”职位 - 活跃信息默认隐藏,需要点开职位详情才能看到
|
1. 一些长时间不活跃的“僵尸”职位 - 活跃信息默认隐藏,需要点开职位详情才能看到
|
||||||
2. 牛头不对马嘴,不符合求职期望的职位
|
2. 牛头不对马嘴,不符合求职期望的职位
|
||||||
|
|
||||||
人工筛选这些职位会有很大的心智负担,真的会吐……
|
人工筛选这些职位会有很大的心智负担,真的会吐……
|
||||||
|
|
||||||
因此,我在Boss炸弹代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位。
|
因此,我在自动开聊代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位。
|
||||||
|
|
||||||
## 使用必读及免责声明
|
## 使用必读及免责声明
|
||||||
如下是使用必读及免责声明,请您务必逐条阅读;本程序首次启动时,您还会再看到一次;若您不接受如下提到的任何内容,请不要使用本程序。
|
如下是使用必读及免责声明,请您务必逐条阅读;本程序首次启动时,您还会再看到一次;若您不接受如下提到的任何内容,请不要使用本程序。
|
||||||
- 本程序从某种程度上说属于辅助工具,与《<a href="https://about.zhipin.com/agreement/?id=registerprotocol_30" rel="noreferer noopener" target="blank">Boss直聘用户协议</a>》(2023年3月版)相关条款相违背,您在注册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**。
|
- 本程序需要存储您的登录凭据,即Cookie,来模拟您在BOSS直聘上开聊BOSS的行为;本程序仅会把您的Cookie存储在本地,并在您访问BOSS直聘时将其传输到BOSS直聘,**不会泄露给第三方**,也不会进行除自动开聊BOSS以外的行为;**请勿向他人泄漏您的Cookie**。
|
||||||
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的Boss直聘账号**进行求职。
|
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的BOSS直聘账号**进行求职。
|
||||||
- 本程序原理是模拟用户在Boss直聘网页上,寻找关键元素并进行点击操作;Boss直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
- 本程序原理是模拟用户在BOSS直聘网页上,寻找关键元素并进行点击操作;BOSS直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
||||||
- 您所在公司可能会对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;上级/HRBP团队可能会从 IT 团队处获取到监控数据,从而了解团队成员离职倾向。如果您不希望上级/HRBP团队了解到您正在求职,建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
|
- 您所在公司可能会对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;上级/HRBP团队可能会从 IT 团队处获取到监控数据,从而了解团队成员离职倾向。如果您不希望上级/HRBP团队了解到您正在求职,建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
|
||||||
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及上级/HRBP报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**。
|
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及上级/HRBP报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**。
|
||||||
- 本程序**没有内置任何付费功能**,**下载**、**使用**是**免费**的,任何人可以**免费获得**、**免费使用**。**作者没有利用本程序赚到过任何收入**。如果您是从GitHub以外的地方付费后“购买”的本程序,或您被提示“必须付费后才能使用本程序”,那**您大概率被骗了**,或者**您下载到了本程序修改版**。**本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方**。
|
- 本程序**没有内置任何付费功能**,**下载**、**使用**是**免费**的,任何人可以**免费获得**、**免费使用**。**作者没有利用本程序赚到过任何收入**。如果您是从GitHub以外的地方付费后“购买”的本程序,或您被提示“必须付费后才能使用本程序”,那**您大概率被骗了**,或者**您下载到了本程序修改版**。**本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方**。
|
||||||
- 本程序**不对您的求职过程与结果负责**,为您开聊的职位均在Boss直聘上发布,职位信息真实性由Boss直聘负责;请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
|
- 本程序**不对您的求职过程与结果负责**,为您开聊的职位均在BOSS直聘上发布,职位信息真实性由BOSS直聘负责;请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
|
||||||
- 请在Boss直聘上自行**屏蔽您不期望投递的公司**;如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃,请**在Boss直聘上屏蔽当前公司及与之关联的公司**。
|
- 请在BOSS直聘上自行**屏蔽您不期望投递的公司**;如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃,请**在BOSS直聘上屏蔽当前公司及与之关联的公司**。
|
||||||
- 本程序经历过了多次测试,理论上来说大部分情况下可以正常运行,但可能也会出现测试用例覆盖不到位,导致程序不按预期运行的情况;如果您有顾虑,建议通过VMware Workstation/Fusion、Oracle VirtualBox、Microsoft Hyper-V等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
- 本程序经历过了多次测试,理论上来说大部分情况下可以正常运行,但可能也会出现测试用例覆盖不到位,导致程序不按预期运行的情况;如果您有顾虑,建议通过VMware Workstation/Fusion、Oracle VirtualBox、Microsoft Hyper-V等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
||||||
|
|
||||||
## 想联系作者?想反馈使用体验上的问题?想找求职搭子?
|
## 想联系作者?想反馈使用体验上的问题?想找求职搭子?
|
||||||
|
|||||||
+4
-2
@@ -18,6 +18,7 @@
|
|||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"puppeteer": "24.19.0",
|
"puppeteer": "24.19.0",
|
||||||
"puppeteer-extra": "3.3.6",
|
"puppeteer-extra": "3.3.6",
|
||||||
|
"puppeteer-extra-plugin-anonymize-ua": "2.4.6",
|
||||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"tapable": "^2.2.1"
|
"tapable": "^2.2.1"
|
||||||
@@ -29,11 +30,12 @@
|
|||||||
"node": "20.16.0"
|
"node": "20.16.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"pnpm": "=8.15.9"
|
"pnpm": ">=8.15.9 <9.0.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,5 +35,6 @@
|
|||||||
],
|
],
|
||||||
"isSageTimeEnabled": true,
|
"isSageTimeEnabled": true,
|
||||||
"sageTimeOpTimes": 100,
|
"sageTimeOpTimes": 100,
|
||||||
"sageTimePauseMinute": 15
|
"sageTimePauseMinute": 15,
|
||||||
|
"blockCompanyNameRegExpStr": ""
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
+1
-4
@@ -1,4 +1 @@
|
|||||||
[
|
[]
|
||||||
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
|
|
||||||
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","巨量引擎","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","小鹅通","富途","京东","沃东天骏","达达","达冠","百度","昆仑芯","小度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","智图","长地万方","瑞图万方","道道通","小熊博望","理想","蔚来","顺丰","丰巢","中通","圆通","申通","跨越","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","淘宝","淘麦郎","天猫","盒马","口碑","优视","夸克","UC","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","51talk","爱学习","高思","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","从鲸","TEMU","得物","有赞","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","虾皮","首汽租车","GoFun","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","锐捷","Tenda","腾达","斐讯","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手","Grab","爱回收","洋钱罐","瓴岳","得到","思维造物","地平线","咪咕","翼支付","电信","天翼","联通","蓝湖","墨刀","海尔","美的","米哈游","传音","同花顺","国美","TCL"
|
|
||||||
]
|
|
||||||
@@ -86,31 +86,32 @@ export const autoStartChatEventBus = new EventEmitter()
|
|||||||
let puppeteer
|
let puppeteer
|
||||||
let StealthPlugin
|
let StealthPlugin
|
||||||
let LaodengPlugin
|
let LaodengPlugin
|
||||||
|
let AnonymizeUaPlugin
|
||||||
export async function initPuppeteer () {
|
export async function initPuppeteer () {
|
||||||
// production
|
// production
|
||||||
const importResult = await Promise.all(
|
const importResult = await Promise.all(
|
||||||
[
|
[
|
||||||
import('puppeteer-extra'),
|
import('puppeteer-extra'),
|
||||||
import('puppeteer-extra-plugin-stealth'),
|
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
|
puppeteer = importResult[0].default
|
||||||
StealthPlugin = importResult[1].default
|
StealthPlugin = importResult[1].default
|
||||||
LaodengPlugin = importResult[2].default
|
LaodengPlugin = importResult[2].default
|
||||||
|
AnonymizeUaPlugin = importResult[3].default
|
||||||
puppeteer.use(StealthPlugin())
|
puppeteer.use(StealthPlugin())
|
||||||
puppeteer.use(LaodengPlugin())
|
puppeteer.use(LaodengPlugin())
|
||||||
|
puppeteer.use(AnonymizeUaPlugin({ makeWindows: false }))
|
||||||
return {
|
return {
|
||||||
puppeteer,
|
puppeteer,
|
||||||
StealthPlugin,
|
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 targetCompanyList = readConfigFile('target-company-list.json').filter(it => !!it.trim());
|
||||||
const combineRecommendJobFilterType = readConfigFile('boss.json').combineRecommendJobFilterType ?? CombineRecommendJobFilterType.ANY_COMBINE
|
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 expectCompanySet = new Set(targetCompanyList)
|
||||||
const enableCompanyAllowList = Boolean(expectCompanySet.size)
|
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 }
|
* @type { import('puppeteer').Browser }
|
||||||
@@ -297,6 +311,18 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
|||||||
let isOptionChosen = false
|
let isOptionChosen = false
|
||||||
if (chooseReasonDialogProxy) {
|
if (chooseReasonDialogProxy) {
|
||||||
switch (reasonCode) {
|
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: {
|
case MarkAsNotSuitReason.BOSS_INACTIVE: {
|
||||||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title="BOSS活跃度低"]`))
|
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title="BOSS活跃度低"]`))
|
||||||
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="职位停招/招满"]`))
|
?? (await chooseReasonDialogProxy.$(`.zp-type-item[title="职位停招/招满"]`))
|
||||||
@@ -311,7 +337,7 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
|||||||
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
|
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
|
||||||
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT: {
|
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT: {
|
||||||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title$="城市"]`))
|
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="面试过/入职过"]`))
|
||||||
?? (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: {
|
case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: {
|
||||||
const opProxy = (await chooseReasonDialogProxy.$(`.zp-type-item[title*="薪资"]`))
|
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*="公司"]`))
|
||||||
?? (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) &&
|
].includes(expectSalaryNotMatchStrategy) &&
|
||||||
strategyScopeOptionWhenMarkSalaryNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
|
strategyScopeOptionWhenMarkSalaryNotMatch === StrategyScopeOptionWhenMarkJobNotMatch.ALL_JOB
|
||||||
) ? !checkIfSalarySuit(it.salaryDesc) : false
|
) ? !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
|
//#region collect not suit reasons
|
||||||
const notSuitReasonIdToStrategyMap = {}
|
const notSuitReasonIdToStrategyMap = {}
|
||||||
const notSuitConditionHandleMap = {
|
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() {
|
async active() {
|
||||||
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
|
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
|
||||||
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
|
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
|
//#region
|
||||||
// null
|
// null
|
||||||
// 刚刚活跃 // 今日活跃 // 昨日活跃 // 3日内活跃 // 本周活跃 // 2周内活跃
|
// 刚刚活跃 // 今日活跃 // 昨日活跃 // 3日内活跃 // 本周活跃 // 2周内活跃
|
||||||
@@ -1544,10 +1624,13 @@ export async function mainLoop (hooks) {
|
|||||||
height: 900 - 140,
|
height: 900 - 140,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
hooks.puppeteerLaunched?.call()
|
hooks.puppeteerLaunched?.call(browser)
|
||||||
page = (await browser.pages())[0]
|
page = (await browser.pages())[0]
|
||||||
|
hooks.pageGotten?.call(page)
|
||||||
//set cookies
|
//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++){
|
for(let i = 0; i < bossCookies.length; i++){
|
||||||
await page.setCookie(bossCookies[i]);
|
await page.setCookie(bossCookies[i]);
|
||||||
}
|
}
|
||||||
@@ -1576,7 +1659,10 @@ export async function closeBrowserWindow () {
|
|||||||
browser?.close()
|
browser?.close()
|
||||||
const browserProcess = browser?.process()
|
const browserProcess = browser?.process()
|
||||||
if (browserProcess) {
|
if (browserProcess) {
|
||||||
process.kill(browserProcess.pid)
|
try {
|
||||||
|
process.kill(browserProcess.pid)
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
}
|
}
|
||||||
browser = null
|
browser = null
|
||||||
page = null
|
page = null
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -32,27 +32,28 @@ async function getEditThisCookieZipPath () {
|
|||||||
return editThisCookieZipPath
|
return editThisCookieZipPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const APP_GEEKGEEKRUN_EDIT_VERSION = 1
|
||||||
export async function ensureEditThisCookie () {
|
export async function ensureEditThisCookie () {
|
||||||
let isNeedExtractEditThisCookie = false
|
let isNeedExtractEditThisCookie = false
|
||||||
const manifestFilePath = path.join(editThisCookieExtensionPath, 'manifest.json')
|
const GEEKGEEKRUN_EDIT_VERSION_FILE_PATH = path.join(editThisCookieExtensionPath, 'GEEKGEEKRUN_EDIT_VERSION')
|
||||||
if (!fs.existsSync(
|
let geekgeekrunEditVersion
|
||||||
manifestFilePath
|
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
|
isNeedExtractEditThisCookie = true
|
||||||
} else {
|
}
|
||||||
let manifest
|
const isExtractDoneFlagFilePath = path.join(editThisCookieExtensionPath, 'EXTRACT_DONE')
|
||||||
try {
|
if (
|
||||||
manifest = JSON.parse(fs.readFileSync(manifestFilePath, { encoding: 'utf-8' }))
|
!isNeedExtractEditThisCookie &&
|
||||||
if (!manifest.manifest_version || manifest.manifest_version <= 2) {
|
!fs.existsSync(isExtractDoneFlagFilePath)
|
||||||
isNeedExtractEditThisCookie = true
|
) {
|
||||||
}
|
isNeedExtractEditThisCookie = true
|
||||||
}
|
|
||||||
catch {
|
|
||||||
console.log(`未能获取到文件内容`)
|
|
||||||
isNeedExtractEditThisCookie = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNeedExtractEditThisCookie) {
|
if (isNeedExtractEditThisCookie) {
|
||||||
if (
|
if (
|
||||||
fs.existsSync(
|
fs.existsSync(
|
||||||
@@ -73,5 +74,9 @@ export async function ensureEditThisCookie () {
|
|||||||
dir: extensionDir
|
dir: extensionDir
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
isExtractDoneFlagFilePath,
|
||||||
|
''
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,8 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
const hooks = {
|
const hooks = {
|
||||||
daemonInitialized: new AsyncSeriesHook(),
|
daemonInitialized: new AsyncSeriesHook(),
|
||||||
puppeteerLaunched: new SyncHook(),
|
puppeteerLaunched: new SyncHook(['browser']),
|
||||||
|
pageGotten: new SyncHook(['page']),
|
||||||
pageLoaded: new SyncHook(),
|
pageLoaded: new SyncHook(),
|
||||||
cookieWillSet: new SyncHook(['cookies']),
|
cookieWillSet: new SyncHook(['cookies']),
|
||||||
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"better-sqlite3": "12.6.2",
|
||||||
"cli-highlight": "^2.1.11",
|
"cli-highlight": "^2.1.11",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"sqlite3": "5.1.6",
|
"typeorm": "0.3.28"
|
||||||
"typeorm": "0.3.11"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.23",
|
||||||
|
"node-gyp": "^12.2.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm();
|
const { Entity, Column, PrimaryGeneratedColumn } = typeorm;
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class AutoStartChatRunRecord {
|
export class AutoStartChatRunRecord {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm();
|
const { Entity, Column, PrimaryGeneratedColumn } = typeorm;
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class BossActiveStatusRecord {
|
export class BossActiveStatusRecord {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryColumn } = requireTypeorm();
|
const { Entity, Column, PrimaryColumn } = typeorm;
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class BossInfo {
|
export class BossInfo {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class BossInfoChangeLog {
|
export class BossInfoChangeLog {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class ChatMessageRecord {
|
export class ChatMessageRecord {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { JobSource } from "../enums";
|
import { JobSource } from "../enums";
|
||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
|
||||||
|
|
||||||
export enum ChatStartupFrom {
|
export enum ChatStartupFrom {
|
||||||
AutoFromRecommendList = null,
|
AutoFromRecommendList = null,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryColumn } = requireTypeorm()
|
const { Entity, Column, PrimaryColumn } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class CompanyInfo {
|
export class CompanyInfo {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, PrimaryGeneratedColumn, Column } = requireTypeorm()
|
const { Entity, PrimaryGeneratedColumn, Column } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class CompanyInfoChangeLog {
|
export class CompanyInfoChangeLog {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { JobHireStatus } from "../enums";
|
import { JobHireStatus } from "../enums";
|
||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryColumn } = requireTypeorm();
|
const { Entity, Column, PrimaryColumn } = typeorm;
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class JobHireStatusRecord {
|
export class JobHireStatusRecord {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryColumn } = requireTypeorm()
|
const { Entity, Column, PrimaryColumn } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class JobInfo {
|
export class JobInfo {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, PrimaryGeneratedColumn, Column } = requireTypeorm()
|
const { Entity, PrimaryGeneratedColumn, Column } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class JobInfoChangeLog {
|
export class JobInfoChangeLog {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, PrimaryGeneratedColumn, Column, Index } = requireTypeorm()
|
const { Entity, PrimaryGeneratedColumn, Column, Index } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(["providerCompleteApiUrl", "model", "providerApiSecret"])
|
@Index(["providerCompleteApiUrl", "model", "providerApiSecret"])
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
|
import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
|
||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
import { ChatStartupFrom } from "./ChatStartupLog";
|
import { ChatStartupFrom } from "./ChatStartupLog";
|
||||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class MarkAsNotSuitLog {
|
export class MarkAsNotSuitLog {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { Entity, Column, PrimaryColumn } = requireTypeorm()
|
const { Entity, Column, PrimaryColumn } = typeorm
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class UserInfo {
|
export class UserInfo {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
const { ViewEntity, ViewColumn } = typeorm;
|
||||||
@ViewEntity({
|
@ViewEntity({
|
||||||
expression: `SELECT
|
expression: `SELECT
|
||||||
boss_info.encryptBossId,
|
boss_info.encryptBossId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
const { ViewEntity, ViewColumn } = typeorm;
|
||||||
@ViewEntity({
|
@ViewEntity({
|
||||||
expression: `SELECT
|
expression: `SELECT
|
||||||
job_info.*,
|
job_info.*,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
const { ViewEntity, ViewColumn } = typeorm;
|
||||||
@ViewEntity({
|
@ViewEntity({
|
||||||
expression: `SELECT
|
expression: `SELECT
|
||||||
company_info.*
|
company_info.*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
const { ViewEntity, ViewColumn } = typeorm;
|
||||||
@ViewEntity({
|
@ViewEntity({
|
||||||
expression: `SELECT
|
expression: `SELECT
|
||||||
job_info.*,
|
job_info.*,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { requireTypeorm } from "../utils/module-loader";
|
import * as typeorm from 'typeorm';
|
||||||
import { ChatStartupFrom } from "./ChatStartupLog";
|
import { ChatStartupFrom } from "./ChatStartupLog";
|
||||||
import { MarkAsNotSuitReason } from "../enums";
|
import { MarkAsNotSuitReason } from "../enums";
|
||||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
const { ViewEntity, ViewColumn } = typeorm;
|
||||||
@ViewEntity({
|
@ViewEntity({
|
||||||
expression: `SELECT
|
expression: `SELECT
|
||||||
job_info.*,
|
job_info.*,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export enum MarkAsNotSuitReason {
|
|||||||
JOB_CITY_NOT_SUIT = 4,
|
JOB_CITY_NOT_SUIT = 4,
|
||||||
JOB_WORK_EXP_NOT_SUIT = 5,
|
JOB_WORK_EXP_NOT_SUIT = 5,
|
||||||
JOB_SALARY_NOT_SUIT = 6,
|
JOB_SALARY_NOT_SUIT = 6,
|
||||||
|
COMPANY_NAME_NOT_SUIT = 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MarkAsNotSuitOp {
|
export enum MarkAsNotSuitOp {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { type DataSource } from "typeorm";
|
import { type DataSource } from "typeorm";
|
||||||
import { requireTypeorm } from "./utils/module-loader";
|
|
||||||
|
|
||||||
import { BossInfo } from "./entity/BossInfo";
|
import { BossInfo } from "./entity/BossInfo";
|
||||||
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
|
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
|
||||||
@@ -22,7 +21,6 @@ import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
|||||||
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
||||||
import { JobHireStatusRecord } from './entity/JobHireStatusRecord'
|
import { JobHireStatusRecord } from './entity/JobHireStatusRecord'
|
||||||
|
|
||||||
import sqlite3 from 'sqlite3';
|
|
||||||
import {
|
import {
|
||||||
saveChatStartupRecord,
|
saveChatStartupRecord,
|
||||||
saveJobInfoFromRecommendPage,
|
saveJobInfoFromRecommendPage,
|
||||||
@@ -40,17 +38,17 @@ import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/17460923
|
|||||||
import { Init1000000000000 } from "./migrations/1000000000000-Init";
|
import { Init1000000000000 } from "./migrations/1000000000000-Init";
|
||||||
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
|
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
|
||||||
import { AddJobHireStatusTable1766466476822 } from "./migrations/1766466476822-AddJobHireStatusTable";
|
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) {
|
export function initDb(dbFilePath) {
|
||||||
const { DataSource } = requireTypeorm()
|
const { DataSource } = typeorm
|
||||||
const appDataSource = new DataSource({
|
const appDataSource = new DataSource({
|
||||||
type: "sqlite",
|
type: "better-sqlite3",
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: true,
|
logging: true,
|
||||||
logger: "simple-console",
|
logger: "simple-console",
|
||||||
database: dbFilePath,
|
database: dbFilePath,
|
||||||
driver: sqlite3, // The important line
|
|
||||||
entities: [
|
entities: [
|
||||||
ChatStartupLog,
|
ChatStartupLog,
|
||||||
BossInfo,
|
BossInfo,
|
||||||
@@ -97,6 +95,25 @@ export default class SqlitePlugin {
|
|||||||
userInfo = null
|
userInfo = null
|
||||||
|
|
||||||
apply(hooks) {
|
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(
|
hooks.userInfoResponse.tapPromise(
|
||||||
"SqlitePlugin",
|
"SqlitePlugin",
|
||||||
async (userInfoResponse) => {
|
async (userInfoResponse) => {
|
||||||
@@ -183,7 +200,6 @@ export default class SqlitePlugin {
|
|||||||
if (chattedJobIds.length === 0) {
|
if (chattedJobIds.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { chunk } = await lodashImportPromise
|
|
||||||
const chattedJobIdChunks = chunk(chattedJobIds, 200)
|
const chattedJobIdChunks = chunk(chattedJobIds, 200)
|
||||||
const chattedBossIds = [];
|
const chattedBossIds = [];
|
||||||
for (const chattedJobIdChunk of chattedJobIdChunks) {
|
for (const chattedJobIdChunk of chattedJobIdChunks) {
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export function requireTypeorm () {
|
|
||||||
const importResult = require('typeorm')
|
|
||||||
return importResult
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,6 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
externalizeDepsPlugin({
|
externalizeDepsPlugin({
|
||||||
exclude: [
|
exclude: [
|
||||||
'@geekgeekrun/geek-auto-start-chat-with-boss',
|
|
||||||
'@geekgeekrun/dingtalk-plugin',
|
|
||||||
'@geekgeekrun/utils',
|
'@geekgeekrun/utils',
|
||||||
'find-chrome-bin',
|
'find-chrome-bin',
|
||||||
'@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
|
'@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
|
||||||
|
|||||||
+10
-12
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "geekgeekrun-ui",
|
"name": "geekgeekrun-ui",
|
||||||
"version": "0.14.1",
|
"version": "0.14.2",
|
||||||
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
|
"description": "BOSS 炸弹 - 自动开聊BOSS,助力每位打工人求职!",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "geekgeekrun",
|
"author": "geekgeekrun",
|
||||||
"productName": "GeekGeekRun",
|
"productName": "GeekGeekRun",
|
||||||
@@ -9,10 +9,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node scripts/run-build-sqlite-plugin.mjs && electron-vite preview",
|
"start": "node scripts/run-build-sqlite-plugin.mjs && electron-vite preview",
|
||||||
"dev": "node scripts/run-build-sqlite-plugin.mjs && electron-vite dev",
|
"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",
|
"build": "node scripts/run-build-sqlite-plugin.mjs && electron-vite build",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||||
@@ -30,16 +26,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^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/launch-bosszhipin-login-page-with-preload-extension": "workspace:*",
|
||||||
|
"@geekgeekrun/pm": "workspace:*",
|
||||||
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
|
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
|
||||||
"@geekgeekrun/sqlite-plugin": "workspace:*",
|
"@geekgeekrun/sqlite-plugin": "workspace:*",
|
||||||
"@geekgeekrun/utils": "workspace:*",
|
"@geekgeekrun/utils": "workspace:*",
|
||||||
"JSONStream": "^1.3.5",
|
"JSONStream": "^1.3.5",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"node-machine-id": "^1.1.12",
|
"node-machine-id": "^1.1.12",
|
||||||
@@ -53,10 +48,13 @@
|
|||||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||||
"@electron-toolkit/tsconfig": "^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/prompts": "^7.0.1",
|
||||||
"@inquirer/select": "^4.0.1",
|
"@inquirer/select": "^4.0.1",
|
||||||
"@puppeteer/browsers": "^2.11.0",
|
"@puppeteer/browsers": "^2.11.0",
|
||||||
"@rushstack/eslint-patch": "^1.7.1",
|
"@rushstack/eslint-patch": "^1.7.1",
|
||||||
|
"@types/lodash": "^4.17.23",
|
||||||
"@types/node": "^18.19.9",
|
"@types/node": "^18.19.9",
|
||||||
"@unocss/preset-attributify": "^0.58.5",
|
"@unocss/preset-attributify": "^0.58.5",
|
||||||
"@unocss/preset-icons": "^0.58.5",
|
"@unocss/preset-icons": "^0.58.5",
|
||||||
@@ -66,13 +64,13 @@
|
|||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "28.2.0",
|
"electron": "39.2.7",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.0.0",
|
||||||
"element-plus": "^2.8.4",
|
"element-plus": "^2.8.4",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-vue": "^9.20.1",
|
"eslint-plugin-vue": "^9.20.1",
|
||||||
"find-chrome-bin": "^2.0.4",
|
"find-chrome-bin": "2.0.4",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
@@ -89,4 +87,4 @@
|
|||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"vue-tsc": "^1.8.27"
|
"vue-tsc": "^1.8.27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "0.14.1",
|
"version": "0.14.2",
|
||||||
"buildVersion": 31,
|
"buildVersion": 32,
|
||||||
"buildTime": 1769428838471,
|
"buildTime": 1770299394728,
|
||||||
"buildHash": "a92b9ce0403a8fc816568cf3eb106a89a6afddf0",
|
"buildHash": "1d5fdb9d54c569cb53035dfef0f6157c899ccb06",
|
||||||
"name": "geekgeekrun-ui"
|
"name": "geekgeekrun-ui"
|
||||||
}
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
|
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
|
||||||
|
export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ export enum AUTO_CHAT_ERROR_EXIT_CODE {
|
|||||||
ERR_INTERNET_DISCONNECTED = 83,
|
ERR_INTERNET_DISCONNECTED = 83,
|
||||||
ACCESS_IS_DENIED = 84,
|
ACCESS_IS_DENIED = 84,
|
||||||
PUPPETEER_IS_NOT_EXECUTABLE = 85,
|
PUPPETEER_IS_NOT_EXECUTABLE = 85,
|
||||||
AUTO_START_CHAT_DAEMON_PROCESS_SUICIDE = 86,
|
LLM_UNAVAILABLE = 86,
|
||||||
AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RECHAT_CONTENT_SOURCE {
|
export enum RECHAT_CONTENT_SOURCE {
|
||||||
@@ -18,3 +17,9 @@ export enum RECHAT_LLM_FALLBACK {
|
|||||||
SEND_LOOK_FORWARD_EMOTION = 1,
|
SEND_LOOK_FORWARD_EMOTION = 1,
|
||||||
EXIT_REMINDER_PROGRAM = 2
|
EXIT_REMINDER_PROGRAM = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum RUNNING_STATUS_ENUM {
|
||||||
|
RUNNING = 0,
|
||||||
|
NORMAL_EXITED = 1,
|
||||||
|
ERROR_EXITED = 2
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: '登录状态检查'
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import os from 'node:os'
|
||||||
|
|
||||||
|
export const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')
|
||||||
@@ -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'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,11 @@ import os from 'os'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import buildInfo from '../../common/build-info.json'
|
import buildInfo from '../../common/build-info.json'
|
||||||
import { ensureStorageFileExist } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
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(
|
export const firstLaunchNoticeApproveFlagPath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
@@ -10,8 +15,28 @@ export const firstLaunchNoticeApproveFlagPath = path.join(
|
|||||||
'ui-first-launch-notice-flag'
|
'ui-first-launch-notice-flag'
|
||||||
)
|
)
|
||||||
|
|
||||||
export const isFirstLaunchNoticeApproveFlagExist = () => fs.existsSync(firstLaunchNoticeApproveFlagPath)
|
export const isFirstLaunchNoticeApproveFlagExist = () =>
|
||||||
|
fs.existsSync(firstLaunchNoticeApproveFlagPath)
|
||||||
export const createFirstLaunchNoticeApproveFlag = () => {
|
export const createFirstLaunchNoticeApproveFlag = () => {
|
||||||
ensureStorageFileExist()
|
ensureStorageFileExist()
|
||||||
fs.writeFileSync(firstLaunchNoticeApproveFlagPath, buildInfo.version)
|
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'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-8
@@ -9,7 +9,7 @@ export enum DOWNLOAD_ERROR_EXIT_CODE {
|
|||||||
DOWNLOAD_ERROR = 80
|
DOWNLOAD_ERROR = 80
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkAndDownloadDependenciesForInit = async () => {
|
export const downloadDependenciesForInit = async () => {
|
||||||
process.on('disconnect', () => app.exit())
|
process.on('disconnect', () => app.exit())
|
||||||
process.on('uncaughtException', () => app.exit(DOWNLOAD_ERROR_EXIT_CODE.DOWNLOAD_ERROR))
|
process.on('uncaughtException', () => app.exit(DOWNLOAD_ERROR_EXIT_CODE.DOWNLOAD_ERROR))
|
||||||
app.dock?.hide()
|
app.dock?.hide()
|
||||||
@@ -50,7 +50,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
|
|||||||
timeoutTimer = setTimeout(() => {
|
timeoutTimer = setTimeout(() => {
|
||||||
// will encounter this when network disconnected when downloading
|
// will encounter this when network disconnected when downloading
|
||||||
promiseWithResolver.reject(new Error('PROGRESS_NOT_CHANGED_TOO_LONG'))
|
promiseWithResolver.reject(new Error('PROGRESS_NOT_CHANGED_TOO_LONG'))
|
||||||
}, 5 * 1000)
|
}, 10 * 1000)
|
||||||
} else {
|
} else {
|
||||||
clearTimeout(throttleProgressTimer)
|
clearTimeout(throttleProgressTimer)
|
||||||
throttleProgressTimer = null
|
throttleProgressTimer = null
|
||||||
@@ -73,7 +73,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
|
|||||||
)
|
)
|
||||||
throttleProgressTimer = setTimeout(() => {
|
throttleProgressTimer = setTimeout(() => {
|
||||||
throttleProgressTimer = null
|
throttleProgressTimer = null
|
||||||
}, 2500)
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -94,11 +94,13 @@ export const checkAndDownloadDependenciesForInit = async () => {
|
|||||||
pipe,
|
pipe,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR',
|
type: 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR',
|
||||||
...err instanceof Error ? {
|
...(err instanceof Error
|
||||||
name: err.name,
|
? {
|
||||||
message: err.message,
|
name: err.name,
|
||||||
stack: err.stack
|
message: err.message,
|
||||||
} : null
|
stack: err.stack
|
||||||
|
}
|
||||||
|
: null)
|
||||||
}) + '\r\n'
|
}) + '\r\n'
|
||||||
)
|
)
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
+3
-10
@@ -8,7 +8,7 @@ export interface BrowserInfo {
|
|||||||
executablePath: string
|
executablePath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_VSERION = 2
|
const CONFIG_VERSION = 2
|
||||||
|
|
||||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||||
export const lastUsedBrowserRecordFilePath = path.join(
|
export const lastUsedBrowserRecordFilePath = path.join(
|
||||||
@@ -35,18 +35,11 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
|
|||||||
!path ||
|
!path ||
|
||||||
!fs.existsSync(path) ||
|
!fs.existsSync(path) ||
|
||||||
!Number(configVersion) ||
|
!Number(configVersion) ||
|
||||||
Number(configVersion) < CONFIG_VSERION
|
Number(configVersion) < CONFIG_VERSION
|
||||||
) {
|
) {
|
||||||
await removeLastUsedAndAvailableBrowserPath()
|
await removeLastUsedAndAvailableBrowserPath()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// blacklist browser
|
|
||||||
if (path.includes(`Microsoft\\Edge\\Application\\msedge.exe`)) {
|
|
||||||
await removeLastUsedAndAvailableBrowserPath()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
executablePath: path,
|
executablePath: path,
|
||||||
browser
|
browser
|
||||||
@@ -64,7 +57,7 @@ export const saveLastUsedAndAvailableBrowserInfo = async (browserInfo: BrowserIn
|
|||||||
}
|
}
|
||||||
await fsPromise.writeFile(
|
await fsPromise.writeFile(
|
||||||
lastUsedBrowserRecordFilePath,
|
lastUsedBrowserRecordFilePath,
|
||||||
[browserInfo.executablePath, browserInfo.browser, CONFIG_VSERION].join('\n')
|
[browserInfo.executablePath, browserInfo.browser, CONFIG_VERSION].join('\n')
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
console.warn('lastUsedBrowserRecordFile write error')
|
console.warn('lastUsedBrowserRecordFile write error')
|
||||||
+30
-25
@@ -1,5 +1,3 @@
|
|||||||
import * as path from 'node:path'
|
|
||||||
import * as os from 'node:os'
|
|
||||||
import * as fs from 'node:fs'
|
import * as fs from 'node:fs'
|
||||||
import type { InstalledBrowser } from '@puppeteer/browsers'
|
import type { InstalledBrowser } from '@puppeteer/browsers'
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +7,8 @@ import {
|
|||||||
removeLastUsedAndAvailableBrowserPath
|
removeLastUsedAndAvailableBrowserPath
|
||||||
} from '../browser-history'
|
} from '../browser-history'
|
||||||
import gtag from '../../../../utils/gtag'
|
import gtag from '../../../../utils/gtag'
|
||||||
|
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../../common/constant'
|
||||||
|
import { cacheDir } from '../../../../constant'
|
||||||
|
|
||||||
const getPuppeteerManagerModule = async () => {
|
const getPuppeteerManagerModule = async () => {
|
||||||
const puppeteerManager = await import('@puppeteer/browsers')
|
const puppeteerManager = await import('@puppeteer/browsers')
|
||||||
@@ -16,8 +16,6 @@ const getPuppeteerManagerModule = async () => {
|
|||||||
return puppeteerManager
|
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 getExpectCachedPuppeteerExecutable = async (): Promise<BrowserInfo> => {
|
||||||
const puppeteerManager = await getPuppeteerManagerModule()
|
const puppeteerManager = await getPuppeteerManagerModule()
|
||||||
@@ -85,41 +83,48 @@ export const checkAndDownloadPuppeteerExecutable = async (
|
|||||||
})
|
})
|
||||||
).find((it) => it.buildId === EXPECT_CHROMIUM_BUILD_ID)!
|
).find((it) => it.buildId === EXPECT_CHROMIUM_BUILD_ID)!
|
||||||
}
|
}
|
||||||
await saveLastUsedAndAvailableBrowserInfo({
|
// await saveLastUsedAndAvailableBrowserInfo({
|
||||||
executablePath: installedBrowser.executablePath,
|
// executablePath: installedBrowser.executablePath,
|
||||||
browser:
|
// browser:
|
||||||
installedBrowser.browser[0].toUpperCase() +
|
// installedBrowser.browser[0].toUpperCase() +
|
||||||
installedBrowser.browser.slice(1) +
|
// installedBrowser.browser.slice(1) +
|
||||||
' ' +
|
// ' ' +
|
||||||
EXPECT_CHROMIUM_BUILD_ID
|
// EXPECT_CHROMIUM_BUILD_ID
|
||||||
})
|
// })
|
||||||
|
|
||||||
return installedBrowser
|
return installedBrowser
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAnyAvailablePuppeteerExecutable = async (): Promise<BrowserInfo | null> => {
|
export const getAnyAvailablePuppeteerExecutable = async ({
|
||||||
const lastUsedOne = await getLastUsedAndAvailableBrowser()
|
ignoreCached = false,
|
||||||
if (lastUsedOne) {
|
noSave = false
|
||||||
return lastUsedOne
|
}: {
|
||||||
|
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
|
// find existed browser - the one maybe actively installed by user or ship with os like Edge on windows
|
||||||
try {
|
try {
|
||||||
const existedOne = await findAndLocateUserInstalledChromiumExecutableSync()
|
const existedOne = await findAndLocateUserInstalledChromiumExecutableSync()
|
||||||
await saveLastUsedAndAvailableBrowserInfo(existedOne)
|
!noSave && (await saveLastUsedAndAvailableBrowserInfo(existedOne))
|
||||||
// save its path
|
// save its path
|
||||||
return existedOne
|
return existedOne
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
console.log('no existed browser path found')
|
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
|
// if no one available, then return null and remove last used browser
|
||||||
await removeLastUsedAndAvailableBrowserPath()
|
await removeLastUsedAndAvailableBrowserPath()
|
||||||
return null
|
return null
|
||||||
@@ -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()
|
|
||||||
-10
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,19 +5,28 @@ import {
|
|||||||
readConfigFile,
|
readConfigFile,
|
||||||
getPublicDbFilePath
|
getPublicDbFilePath
|
||||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||||
|
// import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||||
import * as fs from 'fs'
|
|
||||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
|
||||||
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
|
|
||||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||||
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
|
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
|
||||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||||
|
import minimist from 'minimist'
|
||||||
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
|
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
|
||||||
import gtag from '../../utils/gtag'
|
import gtag from '../../utils/gtag'
|
||||||
import GtagPlugin from '../../utils/gtag/GtagPlugin'
|
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
|
const { default: SqlitePlugin } = SqlitePluginModule
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('收到SIGTERM信号,正在退出')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
const rerunInterval = (() => {
|
const rerunInterval = (() => {
|
||||||
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
|
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
|
||||||
if (isNaN(v)) {
|
if (isNaN(v)) {
|
||||||
@@ -33,14 +42,54 @@ const initPlugins = (hooks) => {
|
|||||||
new DingtalkPlugin(dingTalkAccessToken).apply(hooks)
|
new DingtalkPlugin(dingTalkAccessToken).apply(hooks)
|
||||||
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
|
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
|
||||||
new GtagPlugin().apply(hooks)
|
new GtagPlugin().apply(hooks)
|
||||||
|
// new PeriodPushCurrentPageScreenshotPlugin().apply(hooks)
|
||||||
|
new CookieInvalidHandlePlugin().apply(hooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
let isParentProcessDisconnect = false
|
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
|
||||||
process.once('disconnect', () => {
|
|
||||||
isParentProcessDisconnect = true
|
|
||||||
})
|
|
||||||
|
|
||||||
const runAutoChat = async () => {
|
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(
|
const { initPuppeteer, mainLoop, closeBrowserWindow, autoStartChatEventBus } = await import(
|
||||||
'@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
|
'@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
|
||||||
)
|
)
|
||||||
@@ -48,43 +97,13 @@ const runAutoChat = async () => {
|
|||||||
closeBrowserWindow()
|
closeBrowserWindow()
|
||||||
app.exit()
|
app.exit()
|
||||||
})
|
})
|
||||||
app.dock?.hide()
|
await initPuppeteer()
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const hooks = {
|
const hooks = {
|
||||||
puppeteerLaunched: new SyncHook(),
|
puppeteerLaunched: new SyncHook(['browser']),
|
||||||
|
pageGotten: new SyncHook(['page']),
|
||||||
pageLoaded: new SyncHook(),
|
pageLoaded: new SyncHook(),
|
||||||
cookieWillSet: new SyncHook(['cookies']),
|
cookieWillSet: new AsyncSeriesHook(['cookies']),
|
||||||
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
||||||
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
|
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
|
||||||
jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']),
|
jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']),
|
||||||
@@ -101,23 +120,11 @@ const runAutoChat = async () => {
|
|||||||
initPlugins(hooks)
|
initPlugins(hooks)
|
||||||
|
|
||||||
gtag('run_auto_chat_with_boss_main_ready')
|
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', () => {
|
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 {
|
try {
|
||||||
await mainLoop(hooks)
|
await mainLoop(hooks)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -126,7 +133,7 @@ const runAutoChat = async () => {
|
|||||||
await dialog.showMessageBox({
|
await dialog.showMessageBox({
|
||||||
type: `error`,
|
type: `error`,
|
||||||
message: `登录状态无效`,
|
message: `登录状态无效`,
|
||||||
detail: `请重新登录Boss直聘`
|
detail: `请重新登录BOSS直聘`
|
||||||
})
|
})
|
||||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
|
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
|
||||||
break
|
break
|
||||||
@@ -139,9 +146,18 @@ const runAutoChat = async () => {
|
|||||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED)
|
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED)
|
||||||
break
|
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?.()
|
closeBrowserWindow?.()
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
const shouldExit = await checkShouldExit()
|
||||||
|
if (shouldExit) {
|
||||||
|
app.exit()
|
||||||
|
return
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`[Run core main] An internal error is caught, and browser will be restarted in ${rerunInterval}ms.`
|
`[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 = () => {
|
export const waitForProcessHandShakeAndRunAutoChat = async () => {
|
||||||
let pipe: null | fs.WriteStream = null
|
await app.whenReady()
|
||||||
try {
|
app.on('window-all-closed', (e) => {
|
||||||
pipe = fs.createWriteStream(null, { fd: 3 })
|
e.preventDefault()
|
||||||
} catch {
|
})
|
||||||
console.error('pipe is not available')
|
initPublicIpc()
|
||||||
app.exit(1)
|
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()
|
runAutoChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { app } from 'electron'
|
import { app, dialog } from 'electron'
|
||||||
import { main, loginEventBus } from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
|
import {
|
||||||
|
main,
|
||||||
|
loginEventBus
|
||||||
|
} from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
|
||||||
import { pipeWriteRegardlessError } from './utils/pipe'
|
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 () => {
|
export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
|
||||||
process.on('disconnect', () => app.exit())
|
process.on('disconnect', () => app.exit())
|
||||||
@@ -18,15 +23,26 @@ export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
|
|||||||
type: 'INITIALIZE_PUPPETEER'
|
type: 'INITIALIZE_PUPPETEER'
|
||||||
}) + '\r\n'
|
}) + '\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')
|
const { initPuppeteer } = await import('@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs')
|
||||||
try {
|
try {
|
||||||
await initPuppeteer()
|
await initPuppeteer()
|
||||||
pipeWriteRegardlessError(
|
|
||||||
pipe,
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
|
|
||||||
}) + '\r\n'
|
|
||||||
)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
app.exit(1)
|
app.exit(1)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
;(async () => {
|
||||||
|
await import('@geekgeekrun/pm/daemon.js')
|
||||||
|
})()
|
||||||
|
|
||||||
|
export {}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import './app-menu'
|
|||||||
import initIpc from './ipc'
|
import initIpc from './ipc'
|
||||||
import gtag from '../../utils/gtag'
|
import gtag from '../../utils/gtag'
|
||||||
import initPublicIpc from '../../utils/initPublicIpc'
|
import initPublicIpc from '../../utils/initPublicIpc'
|
||||||
|
import { sendToDaemon, closeDaemonClient } from './connect-to-daemon'
|
||||||
|
|
||||||
export function openSettingWindow() {
|
export function openSettingWindow() {
|
||||||
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
||||||
if (!app.requestSingleInstanceLock()) {
|
if (!app.requestSingleInstanceLock()) {
|
||||||
@@ -65,4 +67,25 @@ export function openSettingWindow() {
|
|||||||
globalShortcut.unregister('Command+Option+Shift+/')
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
import { ipcMain, shell, app } from 'electron'
|
import { ipcMain, shell, app, dialog, BrowserWindow } from 'electron'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import * as childProcess from 'node:child_process'
|
import * as childProcess from 'node:child_process'
|
||||||
import {
|
import {
|
||||||
ensureConfigFileExist,
|
ensureConfigFileExist,
|
||||||
ensureStorageFileExist,
|
|
||||||
configFileNameList,
|
configFileNameList,
|
||||||
readConfigFile,
|
readConfigFile,
|
||||||
writeConfigFile,
|
writeConfigFile,
|
||||||
readStorageFile,
|
readStorageFile,
|
||||||
writeStorageFile,
|
|
||||||
storageFilePath
|
storageFilePath
|
||||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||||
import { ChildProcess } from 'child_process'
|
import { ChildProcess } from 'child_process'
|
||||||
import * as JSONStream from 'JSONStream'
|
import * as JSONStream from 'JSONStream'
|
||||||
import { checkCookieListFormat } from '../../../../common/utils/cookie'
|
import { checkCookieListFormat } from '../../../../common/utils/cookie'
|
||||||
import { getAnyAvailablePuppeteerExecutable } from '../../../flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index'
|
import { getAnyAvailablePuppeteerExecutable } from '../../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 { mainWindow } from '../../../window/mainWindow'
|
import { mainWindow } from '../../../window/mainWindow'
|
||||||
import {
|
import {
|
||||||
getAutoStartChatRecord,
|
getAutoStartChatRecord,
|
||||||
@@ -36,11 +32,11 @@ import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/re
|
|||||||
import {
|
import {
|
||||||
getValidTemplate,
|
getValidTemplate,
|
||||||
requestNewMessageContent
|
requestNewMessageContent
|
||||||
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
|
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
|
||||||
import {
|
import {
|
||||||
autoReminderPromptTemplateFileName,
|
autoReminderPromptTemplateFileName,
|
||||||
writeDefaultAutoRemindPrompt
|
writeDefaultAutoRemindPrompt
|
||||||
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
|
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
|
||||||
import {
|
import {
|
||||||
checkIsResumeContentValid,
|
checkIsResumeContentValid,
|
||||||
resumeContentEnoughDetect
|
resumeContentEnoughDetect
|
||||||
@@ -52,6 +48,16 @@ import {
|
|||||||
import { RequestSceneEnum } from '../../../features/llm-request-log'
|
import { RequestSceneEnum } from '../../../features/llm-request-log'
|
||||||
import { checkUpdateForUi } from '../../../features/updater'
|
import { checkUpdateForUi } from '../../../features/updater'
|
||||||
import gtag from '../../../utils/gtag'
|
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() {
|
export default function initIpc() {
|
||||||
ipcMain.handle('fetch-config-file-content', async () => {
|
ipcMain.handle('fetch-config-file-content', async () => {
|
||||||
@@ -175,6 +181,12 @@ export default function initIpc() {
|
|||||||
if (hasOwn(payload, 'sageTimePauseMinute')) {
|
if (hasOwn(payload, 'sageTimePauseMinute')) {
|
||||||
bossConfig.sageTimePauseMinute = 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))
|
promiseArr.push(writeConfigFile('boss.json', bossConfig))
|
||||||
|
|
||||||
@@ -187,262 +199,111 @@ export default function initIpc() {
|
|||||||
return await Promise.all(promiseArr)
|
return await Promise.all(promiseArr)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('read-storage-file', async (ev, payload) => {
|
ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => {
|
||||||
ensureStorageFileExist()
|
const mode = 'geekAutoStartWithBossMain'
|
||||||
return await readStorageFile(payload.fileName)
|
const { runRecordId } = await runCommon({ mode })
|
||||||
})
|
daemonEE.on('message', function handler(message) {
|
||||||
|
if (message.workerId !== mode) {
|
||||||
ipcMain.handle('write-storage-file', async (ev, payload) => {
|
return
|
||||||
ensureStorageFileExist()
|
}
|
||||||
|
if (message.type === 'worker-exited') {
|
||||||
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
|
mainWindow?.webContents.send('worker-exited', message)
|
||||||
})
|
|
||||||
|
|
||||||
// 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']
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
// 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 () => {
|
ipcMain.handle('run-read-no-reply-auto-reminder', async () => {
|
||||||
if (subProcessOfPuppeteer) {
|
const mode = 'readNoReplyAutoReminderMain'
|
||||||
return
|
const { runRecordId } = await runCommon({ mode })
|
||||||
}
|
daemonEE.on('message', function handler(message) {
|
||||||
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
|
if (message.workerId !== mode) {
|
||||||
if (!puppeteerExecutable) {
|
return
|
||||||
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']
|
|
||||||
}
|
}
|
||||||
)
|
if (message.type === 'worker-exited') {
|
||||||
// console.log(subProcessOfPuppeteer)
|
mainWindow?.webContents.send('worker-exited', message)
|
||||||
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']
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
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 () => {
|
ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => {
|
||||||
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping')
|
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping')
|
||||||
subProcessOfPuppeteer?.kill()
|
const p = new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
daemonEE.on('message', function handler(message) {
|
||||||
try {
|
if (message.workerId !== 'geekAutoStartWithBossMain') {
|
||||||
subProcessOfPuppeteer?.kill('SIGKILL')
|
return
|
||||||
} 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (message.type === 'worker-exited') {
|
||||||
|
daemonEE.off('message', handler)
|
||||||
|
resolve(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await sendToDaemon(
|
||||||
|
{
|
||||||
|
type: 'stop-worker',
|
||||||
|
workerId: 'geekAutoStartWithBossMain'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
needCallback: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
subProcessOfBossZhipinLoginPageWithPreloadExtension!.once('exit', () => {
|
await p
|
||||||
mainWindow?.webContents.send('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED')
|
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
|
||||||
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
ipcMain.on('kill-bosszhipin-login-page-with-preload-extension', async () => {
|
|
||||||
try {
|
ipcMain.handle('stop-read-no-reply-auto-reminder', async () => {
|
||||||
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
|
mainWindow?.webContents.send('read-no-reply-auto-reminder-stopping')
|
||||||
} catch {
|
const p = new Promise((resolve) => {
|
||||||
//
|
daemonEE.on('message', function handler(message) {
|
||||||
} finally {
|
if (message.workerId !== 'readNoReplyAutoReminderMain') {
|
||||||
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
|
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', () => {
|
ipcMain.handle('check-boss-zhipin-cookie-file', () => {
|
||||||
@@ -473,7 +334,7 @@ export default function initIpc() {
|
|||||||
|
|
||||||
let subProcessOfOpenBossSiteDefer: null | PromiseWithResolvers<ChildProcess> = null
|
let subProcessOfOpenBossSiteDefer: null | PromiseWithResolvers<ChildProcess> = null
|
||||||
let subProcessOfOpenBossSite: null | 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
|
const url = data.url
|
||||||
if (
|
if (
|
||||||
!subProcessOfOpenBossSiteDefer ||
|
!subProcessOfOpenBossSiteDefer ||
|
||||||
@@ -481,7 +342,31 @@ export default function initIpc() {
|
|||||||
subProcessOfOpenBossSite.killed
|
subProcessOfOpenBossSite.killed
|
||||||
) {
|
) {
|
||||||
subProcessOfOpenBossSiteDefer = Promise.withResolvers()
|
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 = {
|
const subProcessEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable!.executablePath
|
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable!.executablePath
|
||||||
@@ -666,6 +551,60 @@ export default function initIpc() {
|
|||||||
const newRelease = await checkUpdateForUi()
|
const newRelease = await checkUpdateForUi()
|
||||||
return newRelease
|
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', () => {
|
ipcMain.handle('exit-app-immediately', () => {
|
||||||
app.exit(0)
|
app.exit(0)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export const initDbWorker = () => {
|
|||||||
// attach more event
|
// attach more event
|
||||||
worker?.off('message', handler)
|
worker?.off('message', handler)
|
||||||
} else if (data.type === 'DB_INIT_FAIL') {
|
} else if (data.type === 'DB_INIT_FAIL') {
|
||||||
reject(undefined)
|
reject(data.error)
|
||||||
worker?.terminate()
|
worker?.terminate()
|
||||||
worker?.off('message', handler)
|
worker?.off('message', handler)
|
||||||
worker = null
|
worker = null
|
||||||
@@ -102,3 +102,10 @@ export const getJobHistoryByEncryptId = async (encryptJobId) => {
|
|||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const saveAndGetCurrentRunRecord = async () => {
|
||||||
|
const res = await createWorkerPromise({
|
||||||
|
type: 'saveAndGetCurrentRunRecord'
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { VMarkAsNotSuitLog } from '@geekgeekrun/sqlite-plugin/dist/entity/VMarkA
|
|||||||
import { measureExecutionTime } from '../../../../../../common/utils/performance'
|
import { measureExecutionTime } from '../../../../../../common/utils/performance'
|
||||||
import { PageReq, PagedRes } from '../../../../../../common/types/pagination'
|
import { PageReq, PagedRes } from '../../../../../../common/types/pagination'
|
||||||
import { JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/dist/entity/JobInfoChangeLog'
|
import { JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/dist/entity/JobInfoChangeLog'
|
||||||
|
import { AutoStartChatRunRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/AutoStartChatRunRecord'
|
||||||
|
|
||||||
const dbInitPromise = initDb(getPublicDbFilePath())
|
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||||
let dataSource: DataSource | null = null
|
let dataSource: DataSource | null = null
|
||||||
@@ -161,6 +162,13 @@ const payloadHandler = {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
return data
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -7,8 +7,6 @@ import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storag
|
|||||||
|
|
||||||
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
|
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
|
||||||
const bossChatUiUrl = `https://www.zhipin.com/web/geek/chat`
|
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() {
|
export async function bootstrap() {
|
||||||
const { puppeteer } = await initPuppeteer()
|
const { puppeteer } = await initPuppeteer()
|
||||||
@@ -28,6 +26,8 @@ export async function bootstrap() {
|
|||||||
|
|
||||||
export async function launchBoss(browser: Browser) {
|
export async function launchBoss(browser: Browser) {
|
||||||
const page = (await browser.pages())[0]
|
const page = (await browser.pages())[0]
|
||||||
|
const bossCookies = readStorageFile('boss-cookies.json')
|
||||||
|
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||||||
//set cookies
|
//set cookies
|
||||||
for (let i = 0; i < bossCookies.length; i++) {
|
for (let i = 0; i < bossCookies.length; i++) {
|
||||||
await page.setCookie(bossCookies[i])
|
await page.setCookie(bossCookies[i])
|
||||||
+271
-49
@@ -4,7 +4,6 @@ import { Browser, Page } from 'puppeteer'
|
|||||||
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
|
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
|
||||||
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||||
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
|
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
|
||||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
|
||||||
import { app, dialog } from 'electron'
|
import { app, dialog } from 'electron'
|
||||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||||
import {
|
import {
|
||||||
@@ -17,16 +16,35 @@ import {
|
|||||||
getJobHireStatusRecord,
|
getJobHireStatusRecord,
|
||||||
saveJobHireStatusRecord
|
saveJobHireStatusRecord
|
||||||
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||||
import { writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
import {
|
||||||
import * as fs from 'fs'
|
writeStorageFile,
|
||||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
readStorageFile
|
||||||
|
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
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 gtag from '../../utils/gtag'
|
||||||
import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import cheerio from 'cheerio'
|
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 =
|
const throttleIntervalMinutes =
|
||||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||||
@@ -42,7 +60,23 @@ const rechatLlmFallback =
|
|||||||
|
|
||||||
const expectJobTypeRegExpStr = readConfigFile('boss.json').expectJobTypeRegExpStr
|
const expectJobTypeRegExpStr = readConfigFile('boss.json').expectJobTypeRegExpStr
|
||||||
const onlyRemindBossWithExpectJobType =
|
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())
|
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||||
|
|
||||||
@@ -50,6 +84,30 @@ export const pageMapByName: {
|
|||||||
boss?: Page | null
|
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) {
|
async function saveCurrentChatRecord(page) {
|
||||||
const userInfo = await page.evaluate(
|
const userInfo = await page.evaluate(
|
||||||
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
|
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
|
||||||
@@ -114,7 +172,8 @@ async function saveCurrentChatRecord(page) {
|
|||||||
|
|
||||||
async function checkJobIsClosed() {
|
async function checkJobIsClosed() {
|
||||||
const encryptJobId = await pageMapByName.boss!.evaluate(() => {
|
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) {
|
if (!encryptJobId) {
|
||||||
return false
|
return false
|
||||||
@@ -214,6 +273,44 @@ const mainLoop = async () => {
|
|||||||
browser = null
|
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 = [
|
const canNotConfirmIfHasReadMsgTemplateList = [
|
||||||
'Boss还没查看你的消息',
|
'Boss还没查看你的消息',
|
||||||
'你与该职位竞争者PK情况',
|
'你与该职位竞争者PK情况',
|
||||||
@@ -232,12 +329,45 @@ const mainLoop = async () => {
|
|||||||
// #region
|
// #region
|
||||||
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
|
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
|
||||||
writeStorageFile('boss-cookies.json', [])
|
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 (
|
if (
|
||||||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/403.html') ||
|
currentPageUrl.startsWith('https://www.zhipin.com/web/common/403.html') ||
|
||||||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/error.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')
|
throw new Error('ACCESS_IS_DENIED')
|
||||||
}
|
}
|
||||||
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/safe/verify-slider')) {
|
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/safe/verify-slider')) {
|
||||||
@@ -260,9 +390,31 @@ const mainLoop = async () => {
|
|||||||
})
|
})
|
||||||
if (validateRes.code === 0) {
|
if (validateRes.code === 0) {
|
||||||
await storeStorage(pageMapByName.boss)
|
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')
|
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
|
// #endregion
|
||||||
// check set security question tip modal
|
// check set security question tip modal
|
||||||
let setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$(
|
let setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$(
|
||||||
@@ -296,6 +448,9 @@ const mainLoop = async () => {
|
|||||||
const toCheckItemAtIndex = friendListData.findIndex((it, index) => {
|
const toCheckItemAtIndex = friendListData.findIndex((it, index) => {
|
||||||
return (
|
return (
|
||||||
index >= cursorToContinueFind &&
|
index >= cursorToContinueFind &&
|
||||||
|
(onlyRemindBossWithoutBlockCompanyName && blockCompanyNameRegExp
|
||||||
|
? !blockCompanyNameRegExp.test(it.brandName)
|
||||||
|
: true) &&
|
||||||
(rechatLimitDay && it.updateTime
|
(rechatLimitDay && it.updateTime
|
||||||
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
|
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
|
||||||
: true) &&
|
: true) &&
|
||||||
@@ -452,10 +607,6 @@ const mainLoop = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isParentProcessDisconnect = false
|
|
||||||
process.once('disconnect', () => {
|
|
||||||
isParentProcessDisconnect = true
|
|
||||||
})
|
|
||||||
const rerunInterval = (() => {
|
const rerunInterval = (() => {
|
||||||
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
|
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
|
||||||
if (isNaN(v)) {
|
if (isNaN(v)) {
|
||||||
@@ -465,19 +616,75 @@ const rerunInterval = (() => {
|
|||||||
return v
|
return v
|
||||||
})()
|
})()
|
||||||
|
|
||||||
let pipe
|
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
|
||||||
try {
|
|
||||||
pipe = fs.createWriteStream(null, { fd: 3 })
|
|
||||||
} catch {
|
|
||||||
console.warn('pipe is not available')
|
|
||||||
}
|
|
||||||
export async function runEntry() {
|
export async function runEntry() {
|
||||||
process.on('disconnect', () => {
|
|
||||||
app.exit()
|
|
||||||
})
|
|
||||||
app.dock?.hide()
|
app.dock?.hide()
|
||||||
|
await app.whenReady()
|
||||||
while (!isParentProcessDisconnect) {
|
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 {
|
try {
|
||||||
await mainLoop()
|
await mainLoop()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -487,31 +694,44 @@ export async function runEntry() {
|
|||||||
} catch {
|
} catch {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
// handle error
|
const shouldExit = await checkShouldExit()
|
||||||
if (
|
if (shouldExit) {
|
||||||
err instanceof Error &&
|
app.exit()
|
||||||
['LOGIN_STATUS_INVALID', 'ACCESS_IS_DENIED', 'ERR_INTERNET_DISCONNECTED'].includes(
|
return
|
||||||
err.message
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
pipeWriteRegardlessError(
|
|
||||||
pipe,
|
|
||||||
JSON.stringify({
|
|
||||||
type: err.message
|
|
||||||
}) + '\r\n'
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
if (err instanceof Error && err.message === 'CANNOT_FIND_A_USABLE_MODEL') {
|
// handle error
|
||||||
gtag('cannot_find_a_usable_model')
|
if (err instanceof Error) {
|
||||||
await dialog.showMessageBox({
|
if (err.message.includes('LOGIN_STATUS_INVALID')) {
|
||||||
type: 'error',
|
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
|
||||||
message:
|
break
|
||||||
'未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题',
|
}
|
||||||
buttons: ['退出']
|
if (err.message.includes('ERR_INTERNET_DISCONNECTED')) {
|
||||||
})
|
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ERR_INTERNET_DISCONNECTED)
|
||||||
process.exit(0)
|
break
|
||||||
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 {
|
} finally {
|
||||||
pageMapByName['boss'] = null
|
pageMapByName['boss'] = null
|
||||||
@@ -522,8 +742,6 @@ export async function runEntry() {
|
|||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
attachListenerForKillSelfOnParentExited()
|
|
||||||
|
|
||||||
process.once('uncaughtException', (error) => {
|
process.once('uncaughtException', (error) => {
|
||||||
console.error('uncaughtException', error)
|
console.error('uncaughtException', error)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
@@ -533,6 +751,10 @@ process.once('unhandledRejection', (error) => {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
process.once('disconnect', () => {
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
async function storeStorage(page) {
|
async function storeStorage(page) {
|
||||||
const [cookies, localStorage] = await Promise.all([
|
const [cookies, localStorage] = await Promise.all([
|
||||||
page.cookies(),
|
page.cookies(),
|
||||||
@@ -1,12 +1,25 @@
|
|||||||
import minimist from 'minimist'
|
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 isUiDev = process.env.NODE_ENV === 'development'
|
||||||
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
|
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
|
||||||
console.log(commandlineArgs)
|
console.log(commandlineArgs)
|
||||||
|
|
||||||
const runMode = commandlineArgs['mode'];
|
const runMode = commandlineArgs['mode']
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
switch (runMode) {
|
switch (runMode) {
|
||||||
|
// #region internal use
|
||||||
case 'geekAutoStartWithBossMain': {
|
case 'geekAutoStartWithBossMain': {
|
||||||
const { waitForProcessHandShakeAndRunAutoChat } = await import(
|
const { waitForProcessHandShakeAndRunAutoChat } = await import(
|
||||||
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index'
|
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index'
|
||||||
@@ -14,18 +27,9 @@ const runMode = commandlineArgs['mode'];
|
|||||||
waitForProcessHandShakeAndRunAutoChat()
|
waitForProcessHandShakeAndRunAutoChat()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'geekAutoStartWithBossDaemon': {
|
case 'downloadDependenciesForInit': {
|
||||||
const { runAutoChatWithDaemon } = await import(
|
const { downloadDependenciesForInit } = await import('./flow/DOWNLOAD_DEPENDENCIES/index')
|
||||||
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/index'
|
downloadDependenciesForInit()
|
||||||
)
|
|
||||||
runAutoChatWithDaemon()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'checkAndDownloadDependenciesForInit': {
|
|
||||||
const { checkAndDownloadDependenciesForInit } = await import(
|
|
||||||
'./flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/index'
|
|
||||||
)
|
|
||||||
checkAndDownloadDependenciesForInit()
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'launchBossZhipinLoginPageWithPreloadExtension': {
|
case 'launchBossZhipinLoginPageWithPreloadExtension': {
|
||||||
@@ -40,15 +44,43 @@ const runMode = commandlineArgs['mode'];
|
|||||||
launchBossSite()
|
launchBossSite()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'readNoReplyAutoReminder': {
|
case 'readNoReplyAutoReminderMain': {
|
||||||
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER/index')
|
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER_MAIN/index')
|
||||||
runEntry()
|
runEntry()
|
||||||
break
|
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: {
|
default: {
|
||||||
|
globalThis.GEEKGEEKRUN_PROCESS_ROLE = 'ui'
|
||||||
|
await launchDaemon()
|
||||||
const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index')
|
const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index')
|
||||||
openSettingWindow()
|
openSettingWindow()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// #region
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { BrowserWindow, ipcMain, shell } from 'electron'
|
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
|
||||||
import gtag from './gtag'
|
import gtag from './gtag'
|
||||||
import buildInfo from '../../common/build-info.json'
|
import buildInfo from '../../common/build-info.json'
|
||||||
import os from 'node:os'
|
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() {
|
export default function initPublicIpc() {
|
||||||
ipcMain.on(
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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!
|
||||||
|
}
|
||||||
@@ -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!
|
||||||
|
}
|
||||||
@@ -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!
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { BrowserWindow, ipcMain } from 'electron'
|
import { BrowserWindow } from 'electron'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { createFirstLaunchNoticeApproveFlag } from '../features/first-launch-notice-window'
|
|
||||||
|
|
||||||
export let firstLaunchNoticeWindow: BrowserWindow | null = null
|
export let firstLaunchNoticeWindow: BrowserWindow | null = null
|
||||||
export function createFirstLaunchNoticeWindow(
|
export function createFirstLaunchNoticeWindow(
|
||||||
@@ -41,11 +40,3 @@ export function createFirstLaunchNoticeWindow(
|
|||||||
|
|
||||||
return firstLaunchNoticeWindow!
|
return firstLaunchNoticeWindow!
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initIpc = () => {
|
|
||||||
ipcMain.handle('first-launch-notice-approve', () => {
|
|
||||||
createFirstLaunchNoticeApproveFlag()
|
|
||||||
firstLaunchNoticeWindow?.close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
initIpc()
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { BrowserWindow, shell } from 'electron'
|
import { BrowserWindow, shell } from 'electron'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { openDevTools } from '../commands'
|
import { openDevTools } from '../commands'
|
||||||
import { createFirstLaunchNoticeWindow } from './firstLaunchNoticeWindow'
|
import { daemonEE } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||||
import { isFirstLaunchNoticeApproveFlagExist } from '../features/first-launch-notice-window'
|
|
||||||
export let mainWindow: BrowserWindow | null = null
|
export let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
export function createMainWindow(): BrowserWindow {
|
export function createMainWindow(): BrowserWindow {
|
||||||
@@ -27,14 +26,6 @@ export function createMainWindow(): BrowserWindow {
|
|||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
mainWindow.show()
|
mainWindow.show()
|
||||||
})
|
})
|
||||||
mainWindow.on('ready-to-show', async () => {
|
|
||||||
!isFirstLaunchNoticeApproveFlagExist() &&
|
|
||||||
createFirstLaunchNoticeWindow({
|
|
||||||
parent: mainWindow!,
|
|
||||||
modal: true,
|
|
||||||
show: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
mainWindow.on('ready-to-show', async () => {
|
mainWindow.on('ready-to-show', async () => {
|
||||||
process.env.NODE_ENV === 'development' &&
|
process.env.NODE_ENV === 'development' &&
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -58,5 +49,13 @@ export function createMainWindow(): BrowserWindow {
|
|||||||
mainWindow!.once('closed', () => {
|
mainWindow!.once('closed', () => {
|
||||||
mainWindow = null
|
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!
|
return mainWindow!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { random } from 'lodash-es'
|
import { random } from 'lodash'
|
||||||
|
|
||||||
const rowCount = 4
|
const rowCount = 4
|
||||||
const colCount = 6
|
const colCount = 6
|
||||||
|
|||||||
@@ -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.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.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="职位分类">{{ 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-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-divider content-position="left">变更记录</el-divider>
|
<el-divider content-position="left">变更记录</el-divider>
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
modal-class="running-overlay__modal"
|
||||||
|
:model-value="isDialogVisible"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:show-close="false"
|
||||||
|
width="400px"
|
||||||
|
@closed="fillEmptySteps"
|
||||||
|
>
|
||||||
|
<div flex flex-col flex-items-center>
|
||||||
|
<div class="dialog-header" w-full>
|
||||||
|
<div
|
||||||
|
h160px
|
||||||
|
w-full
|
||||||
|
:style="{
|
||||||
|
backgroundImage: 'linear-gradient(#666, #666)'
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-main" w-full mt--20px>
|
||||||
|
<!-- 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>
|
||||||
|
</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'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
const props = defineProps({
|
||||||
|
workerId: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
runRecordId: {
|
||||||
|
type: Number
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// const taskManagerStore = useTaskManagerStore()
|
||||||
|
// const runningTaskInfo = 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
|
||||||
|
})
|
||||||
|
watch(
|
||||||
|
() => stepsForRender.value,
|
||||||
|
(v) => {
|
||||||
|
const rejectedItems = v?.filter((it) => it.status === 'rejected')
|
||||||
|
if (!rejectedItems.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gtagRenderer('running_overlay_rejected', {
|
||||||
|
stepId: rejectedItems.map((it) => it.id).join(','),
|
||||||
|
workerId: props.workerId
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ deep: 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
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => isDialogVisible.value,
|
||||||
|
(newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
gtagRenderer('running_overlay_shown')
|
||||||
|
} else {
|
||||||
|
gtagRenderer('running_overlay_hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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
|
||||||
|
gtagRenderer('running_overlay_error_exited', {
|
||||||
|
exitCode: code,
|
||||||
|
workerId: props.workerId
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
currentRunningStatus.value = RUNNING_STATUS_ENUM.NORMAL_EXITED
|
||||||
|
gtagRenderer('running_overlay_normal_exited', {
|
||||||
|
exitCode: code,
|
||||||
|
workerId: props.workerId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.el-overlay.running-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;
|
||||||
|
}
|
||||||
|
.dialog-header {
|
||||||
|
display: none;
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: center;
|
||||||
|
// border-radius: 20px 20px 0 0;
|
||||||
|
// overflow: hidden;
|
||||||
|
}
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
.dialog-main {
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--el-dialog-bg-color);
|
||||||
|
box-shadow: var(--el-dialog-box-shadow);
|
||||||
|
padding: var(--el-dialog-padding-primary);
|
||||||
|
//border-radius: 0 0 20px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -4,56 +4,37 @@
|
|||||||
<img
|
<img
|
||||||
class="block"
|
class="block"
|
||||||
:class="{
|
:class="{
|
||||||
'animate__animated animate__bounce animate__repeat-3':
|
'animate__animated animate__bounce animate__repeat-3': true
|
||||||
Object.values(checkDependenciesResult).includes(false)
|
|
||||||
}"
|
}"
|
||||||
:width="256"
|
:width="256"
|
||||||
src="@renderer/../../../resources/icon.png"
|
src="@renderer/../../../resources/icon.png"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div mt24px>愿你薪想事成</div>
|
<div mt24px>愿你薪想事成</div>
|
||||||
<div class="h60px mt14px">
|
|
||||||
<RouterView
|
|
||||||
class="h100%"
|
|
||||||
:dependencies-status="checkDependenciesResult"
|
|
||||||
:process-waitee="downloadProcessWaitee"
|
|
||||||
></RouterView>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const checkDependenciesResult = ref({})
|
// const checkDependenciesResult = ref({})
|
||||||
const downloadProcessWaitee = ref(null)
|
// const downloadProcessWaitee = ref(null)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
gtagRenderer('bootstrap_mounted')
|
gtagRenderer('bootstrap_mounted')
|
||||||
checkDependenciesResult.value = await electron.ipcRenderer.invoke('check-dependencies')
|
await sleep(1500)
|
||||||
downloadProcessWaitee.value = Promise.withResolvers()
|
try {
|
||||||
|
await electron.ipcRenderer.invoke('pre-enter-setting-ui')
|
||||||
if (Object.values(checkDependenciesResult.value).includes(false)) {
|
} catch (err) {
|
||||||
gtagRenderer('dependencies_need_download')
|
console.log('pre-enter-setting-ui error', err)
|
||||||
router.replace('/downloadingDependencies')
|
} finally {
|
||||||
} else {
|
await sleep(500)
|
||||||
downloadProcessWaitee.value!.resolve()
|
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>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
<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 ref="faqMainRef" class="faq-main">
|
||||||
|
<details class="faq-item" data-faq-id="cannot-auto-find-executable">
|
||||||
|
<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.7)Google Chrome 最新版本为
|
||||||
|
144.0.7559.133
|
||||||
|
,多数情况下本程序都可以正常工作,但由于浏览器会自动升级,版本不固定,可能存在<span
|
||||||
|
color-orange
|
||||||
|
>浏览器升级后某些功能不兼容导致本程序不能正确运行</span
|
||||||
|
>的问题。如果您确实遇到不能正常运行的问题,请<a
|
||||||
|
href="javascript:;"
|
||||||
|
@click="handleFeedbackClick"
|
||||||
|
>提交 Issue</a
|
||||||
|
>来反馈,同时请再尝试方案一。
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details class="faq-item" data-faq-id="manual-select-browser-prerequisite">
|
||||||
|
<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 内核浏览器,因此 Safari、Firefox、Windows 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"
|
||||||
|
data-faq-id="what-will-happen-when-select-unsupported-browser"
|
||||||
|
>
|
||||||
|
<summary>如果我选择了一个不支持的浏览器,会发生什么?</summary>
|
||||||
|
<div class="faq-answer">
|
||||||
|
<div>可能会发生的情况:</div>
|
||||||
|
<ul>
|
||||||
|
<li>浏览器将会启动,但会开启一个空白页面,之后浏览器不会做任何事</li>
|
||||||
|
<li>浏览器启动失败,你不会看到任何界面</li>
|
||||||
|
<li>浏览器会每隔一段时间打开一个新的浏览器窗口</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details class="faq-item" data-faq-id="what-will-happen-when-select-other-file">
|
||||||
|
<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, onMounted, 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: 'browser-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() {
|
||||||
|
gtagRenderer('auto_detect_pptr_exe_clicked')
|
||||||
|
isAutoDetectLoading.value = true
|
||||||
|
await sleep(50)
|
||||||
|
try {
|
||||||
|
const result = await ipcRenderer.invoke('get-any-available-puppeteer-executable', {
|
||||||
|
ignoreCached: true,
|
||||||
|
noSave: true
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
gtagRenderer('auto_detect_pptr_exe_not_found')
|
||||||
|
ElMessage({
|
||||||
|
message: '未检测到可用浏览器的可执行文件',
|
||||||
|
type: 'warning',
|
||||||
|
grouping: true
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gtagRenderer('auto_detect_pptr_exe_done', {
|
||||||
|
isUseCached: !!(
|
||||||
|
result.executablePath?.includes(`cache`) && result.executablePath?.includes(`.geekgeekrun`)
|
||||||
|
),
|
||||||
|
executableName: result.executablePath?.split(/\/|\\/).pop() ?? ''
|
||||||
|
})
|
||||||
|
formData.value.browserPath = result.executablePath
|
||||||
|
ElMessage({
|
||||||
|
message: '已找到可用浏览器,可执行文件路径已填入输入框',
|
||||||
|
type: 'success',
|
||||||
|
grouping: true
|
||||||
|
})
|
||||||
|
await nextTick()
|
||||||
|
await formRef.value.validateField()
|
||||||
|
} finally {
|
||||||
|
isAutoDetectLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseExecutableFile() {
|
||||||
|
gtagRenderer('choose_pptr_exe_clicked')
|
||||||
|
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) {
|
||||||
|
gtagRenderer('choose_pptr_exe_cancelled')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formData.value.browserPath = chooseResult.filePaths[0]
|
||||||
|
gtagRenderer('choose_pptr_exe_done', {
|
||||||
|
executableName: chooseResult.filePaths[0]?.split(/\/|\\/).pop() ?? ''
|
||||||
|
})
|
||||||
|
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() {
|
||||||
|
gtagRenderer('save_clicked', {
|
||||||
|
executablePath: formData.value.browserPath,
|
||||||
|
executableName: formData.value.browserPath?.split(/\/|\\/).pop() ?? ''
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
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')
|
||||||
|
gtagRenderer('save_done', {
|
||||||
|
executablePath: formData.value.browserPath,
|
||||||
|
executableName: formData.value.browserPath?.split(/\/|\\/).pop() ?? ''
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
gtagRenderer('save_validate_failed', {
|
||||||
|
error: err?.message ?? '',
|
||||||
|
executablePath: formData.value.browserPath,
|
||||||
|
executableName: formData.value.browserPath?.split(/\/|\\/).pop() ?? ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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')
|
||||||
|
let downloadedBrowserPath
|
||||||
|
try {
|
||||||
|
downloadedBrowserPath = await electron.ipcRenderer.invoke('download-browser-with-downloader')
|
||||||
|
if (downloadedBrowserPath) {
|
||||||
|
formData.value.browserPath = downloadedBrowserPath
|
||||||
|
ElMessage({
|
||||||
|
message: '浏览器下载成功,可执行文件路径已填入输入框',
|
||||||
|
type: 'success',
|
||||||
|
grouping: true
|
||||||
|
})
|
||||||
|
gtagRenderer('browser_downloader_done_with_path')
|
||||||
|
} else {
|
||||||
|
ElMessage({
|
||||||
|
message:
|
||||||
|
'浏览器下载成功,但未返回可执行文件路径。请点击自动检测,或手动选择~/.geekgeekrun/cache/chrome文件夹下的文件,或重新下载',
|
||||||
|
type: 'success',
|
||||||
|
grouping: true
|
||||||
|
})
|
||||||
|
gtagRenderer('browser_downloader_done_without_path')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
gtagRenderer('browser_downloader_cancelled')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const faqMainRef = ref()
|
||||||
|
onMounted(() => {
|
||||||
|
const faqItemEls = faqMainRef?.value?.querySelectorAll(`details`) ?? []
|
||||||
|
for (const el of faqItemEls) {
|
||||||
|
el.addEventListener('toggle', () => {
|
||||||
|
const isOpen = el.open
|
||||||
|
gtagRenderer('faq_item_toggled', {
|
||||||
|
faqId: el.dataset.faqId,
|
||||||
|
isOpen
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</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>
|
||||||
@@ -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>
|
||||||
+14
-14
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<p>核心组件下载失败,请重试。</p>
|
<p>浏览器下载失败,请重试,或使用其他方式下载</p>
|
||||||
<br />
|
<!-- <br />
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<p>
|
||||||
<b text-orange>提示:</b>由于网络颠簸,如果多次重试仍然失败,请 <el-button
|
<b text-orange>提示:</b>由于网络颠簸,如果多次重试仍然失败,请 <el-button
|
||||||
@@ -12,23 +12,23 @@
|
|||||||
>点击此处</el-button
|
>点击此处</el-button
|
||||||
> 下载最新版本 Google Chrome
|
> 下载最新版本 Google Chrome
|
||||||
浏览器,安装完毕后,重新打开本程序,程序会自动检测该浏览器并使用它。
|
浏览器,安装完毕后,重新打开本程序,程序会自动检测该浏览器并使用它。
|
||||||
</p>
|
</p> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
// import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
import debounce from 'lodash-es/debounce'
|
// import debounce from 'lodash/debounce'
|
||||||
const { ipcRenderer } = electron
|
// const { ipcRenderer } = electron
|
||||||
|
|
||||||
const handleOpenChromeDownloadPage = debounce(
|
// const handleOpenChromeDownloadPage = debounce(
|
||||||
async () => {
|
// async () => {
|
||||||
gtagRenderer('open_chrome_download_page_clicked')
|
// gtagRenderer('open_chrome_download_page_clicked')
|
||||||
ipcRenderer.send('open-external-link', 'https://www.google.cn/chrome/')
|
// ipcRenderer.send('open-external-link', 'https://www.google.cn/chrome/')
|
||||||
},
|
// },
|
||||||
1000,
|
// 1000,
|
||||||
{ leading: true, trailing: false }
|
// { leading: true, trailing: false }
|
||||||
)
|
// )
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<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 as baseGtagRenderer } 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 gtagRenderer = (name, params?: object) => {
|
||||||
|
return baseGtagRenderer(name, {
|
||||||
|
scene: 'browser-download-progress',
|
||||||
|
...params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
gtagRenderer('download_deps_done')
|
||||||
|
} 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>
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cookie-assistant-page">
|
<div class="cookie-assistant-page">
|
||||||
<div ml1em mt1em mb1em font-size-16px>Boss 登录助手</div>
|
<div ml1em mt1em mb1em font-size-16px>BOSS登录助手</div>
|
||||||
<el-alert
|
<el-alert v-if="cookieInvalid" type="warning" :closable="false">
|
||||||
v-if="cookieInvalid"
|
由于您是首次使用本程序,或者您之前使用的BOSS直聘账号登录状态失效,因此您需要重新获取登录凭证
|
||||||
type="warning"
|
|
||||||
:closable="false"
|
|
||||||
title="需要获取您的Boss直聘Cookie才能继续"
|
|
||||||
>
|
|
||||||
由于您是首次使用本程序,或者您之前使用的Boss直聘账号登录状态失效,因此您需要重新获取登录凭证。
|
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<div ml1em mt1em line-height-normal>
|
<div ml1em mt1em line-height-normal>
|
||||||
如果您了解如何获取Cookie、了解有效的Cookie格式,可以直接在下方输入框中进行编辑。由于手动编辑较为麻烦,建议您打开已登录过Boss直聘的浏览器,使用<a
|
如果您了解如何获取Cookie、了解有效的Cookie格式,可以直接在下方输入框中进行编辑。由于手动编辑较为麻烦,建议您打开已登录过BOSS直聘的浏览器,使用<a
|
||||||
class="color-blue! decoration-none"
|
class="color-blue! decoration-none"
|
||||||
href="javascript:void(0)"
|
href="javascript:void(0)"
|
||||||
@click.prevent="handleEditThisCookieExtensionStoreLinkClick"
|
@click.prevent="handleEditThisCookieExtensionStoreLinkClick"
|
||||||
@@ -29,14 +24,14 @@
|
|||||||
>
|
>
|
||||||
启动浏览器
|
启动浏览器
|
||||||
</li>
|
</li>
|
||||||
<li>按照正常流程,通过 <b>短信验证码/二维码/微信小程序</b> 登录您的Boss直聘账号</li>
|
<li>按照正常流程,通过 <b>短信验证码/二维码/微信小程序</b> 登录您的BOSS直聘账号</li>
|
||||||
<li>接下来将自动进行一些页面跳转,最终将会停留在首页</li>
|
<li>接下来将自动进行一些页面跳转,最终将会停留在首页</li>
|
||||||
<li>
|
<li>
|
||||||
登录后预计5-10秒内(具体取决于您的网速),您的Cookie将被自动填入下方输入框。
|
登录后预计5-10秒内(具体取决于您的网速),您的Cookie将被自动填入下方输入框。
|
||||||
<details>
|
<details>
|
||||||
<summary color-orange cursor-pointer>我已完成登录,但Cookie一直没出现?</summary>
|
<summary color-orange cursor-pointer>我已完成登录,但Cookie一直没出现?</summary>
|
||||||
<div ml-2em max-h-200px of-auto>
|
<div ml-2em max-h-200px of-auto>
|
||||||
如果您确实已经在打开浏览器中看到您已登录了Boss直聘,请尝试按照如图所示方式复制Cookie:
|
如果您确实已经在打开浏览器中看到您已登录了BOSS直聘,请尝试按照如图所示方式复制Cookie:
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption>依次点击浏览器右上角“扩展程序”图标、“EditThisCookie”图标</figcaption>
|
<figcaption>依次点击浏览器右上角“扩展程序”图标、“EditThisCookie”图标</figcaption>
|
||||||
<img block max-w-full src="./resources/copy-cookie-step-1.png" />
|
<img block max-w-full src="./resources/copy-cookie-step-1.png" />
|
||||||
@@ -98,7 +93,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<footer flex mt20px pb20px flex-justify-end>
|
<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>
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,7 +196,7 @@ const handleEditThisCookieExtensionStoreLinkClick = () => {
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
gtagRenderer('cancel_clicked')
|
gtagRenderer('cancel_clicked')
|
||||||
router.replace('/main-layout')
|
window.close()
|
||||||
}
|
}
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
gtagRenderer('save_clicked')
|
gtagRenderer('save_clicked')
|
||||||
@@ -210,9 +205,10 @@ const handleSubmit = async () => {
|
|||||||
fileName: 'boss-cookies.json',
|
fileName: 'boss-cookies.json',
|
||||||
data: formContent.value.collectedCookies
|
data: formContent.value.collectedCookies
|
||||||
})
|
})
|
||||||
ElMessage.success('Boss直聘 Cookie 保存成功')
|
ElMessage.success('BOSS直聘 Cookie 保存成功')
|
||||||
gtagRenderer('save_cookie_done')
|
gtagRenderer('save_cookie_done')
|
||||||
router.replace('/main-layout')
|
|
||||||
|
window.electron.ipcRenderer.send('cookie-saved')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBossZhipinLoginPageClosed = () => {
|
const handleBossZhipinLoginPageClosed = () => {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
gtagRenderer('view_boss_agreement_clicked')
|
gtagRenderer('view_boss_agreement_clicked')
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>《Boss直聘用户协议》</el-link
|
>《BOSS直聘用户协议》</el-link
|
||||||
>(2023年3月版)相关条款相违背,您在注册Boss直聘时已签署过这一条款;根据该条款
|
>(2023年3月版)相关条款相违背,您在注册BOSS直聘时已签署过这一条款;根据该条款
|
||||||
<i>七、用户的平台使用义务</i>、<i>八、违约责任</i>
|
<i>七、用户的平台使用义务</i>、<i>八、违约责任</i>
|
||||||
章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于<b class="color-red"
|
章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于<b class="color-red"
|
||||||
>账号被强制退出登录、账号被限制使用、账号被封禁</b
|
>账号被强制退出登录、账号被限制使用、账号被封禁</b
|
||||||
@@ -31,10 +31,10 @@
|
|||||||
>,且如果相关风险发生,<b class="color-red">您需要自行承担相关后果,本程序不负责</b>。
|
>,且如果相关风险发生,<b class="color-red">您需要自行承担相关后果,本程序不负责</b>。
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
|
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
|
||||||
本程序需要存储您的登录凭据,即Cookie,来模拟您在Boss直聘上开聊Boss的行为;本程序仅会把您的Cookie存储在本地,并在您访问Boss直聘时将其传输到Boss直聘,<b
|
本程序需要存储您的登录凭据,即Cookie,来模拟您在BOSS直聘上开聊BOSS的行为;本程序仅会把您的Cookie存储在本地,并在您访问BOSS直聘时将其传输到BOSS直聘,<b
|
||||||
class="color-red"
|
class="color-red"
|
||||||
>不会泄露给第三方</b
|
>不会泄露给第三方</b
|
||||||
>,也不会进行除自动开聊Boss以外的行为;<b class="color-red">请勿向他人泄漏您的Cookie</b
|
>,也不会进行除自动开聊BOSS以外的行为;<b class="color-red">请勿向他人泄漏您的Cookie</b
|
||||||
>。
|
>。
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
|
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
|
||||||
@@ -42,11 +42,11 @@
|
|||||||
class="color-red"
|
class="color-red"
|
||||||
>注意节制</b
|
>注意节制</b
|
||||||
>,建议当天开聊次数用尽后,隔几天再使用。建议您<b class="color-red"
|
>,建议当天开聊次数用尽后,隔几天再使用。建议您<b class="color-red"
|
||||||
>注册一个本程序专用的新的Boss直聘账号</b
|
>注册一个本程序专用的新的BOSS直聘账号</b
|
||||||
>进行求职。
|
>进行求职。
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
|
<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"
|
class="color-red"
|
||||||
>浏览器重复“闪退、重新启动”</b
|
>浏览器重复“闪退、重新启动”</b
|
||||||
>)。如果您在使用过程中遇上程序未按照预期执行的情况,请点击程序左下角进行反馈。
|
>)。如果您在使用过程中遇上程序未按照预期执行的情况,请点击程序左下角进行反馈。
|
||||||
@@ -81,16 +81,16 @@
|
|||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
|
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
|
||||||
本程序<b class="color-red">不对您的求职过程与结果负责</b
|
本程序<b class="color-red">不对您的求职过程与结果负责</b
|
||||||
>,为您开聊的职位均在Boss直聘上发布,职位信息真实性由Boss直聘负责;请<b
|
>,为您开聊的职位均在BOSS直聘上发布,职位信息真实性由BOSS直聘负责;请<b
|
||||||
class="color-red"
|
class="color-red"
|
||||||
>自行甄别为您开聊的公司、认真决定是否参加面试、慎重选择Offer</b
|
>自行甄别为您开聊的公司、认真决定是否参加面试、慎重选择Offer</b
|
||||||
>。
|
>。
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
|
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
|
||||||
请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
|
请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
|
||||||
>;如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃,请<b
|
>;如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃,请<b
|
||||||
class="color-red"
|
class="color-red"
|
||||||
>在Boss直聘上屏蔽当前公司及与之关联的公司</b
|
>在BOSS直聘上屏蔽当前公司及与之关联的公司</b
|
||||||
>。
|
>。
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
|
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
|
||||||
@@ -129,7 +129,7 @@ const readmeItemCheckStatusList = ref<number[]>([])
|
|||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
gtagRenderer('cancel_clicked')
|
gtagRenderer('cancel_clicked')
|
||||||
await sleep(500)
|
await sleep(500)
|
||||||
electron.ipcRenderer.invoke('exit-app-immediately')
|
window.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
|
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
|
||||||
@@ -150,7 +150,7 @@ const handleSubmit = () => {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
electron.ipcRenderer.invoke('first-launch-notice-approve')
|
electron.ipcRenderer.send('first-launch-notice-approve')
|
||||||
gtagRenderer('submit_done')
|
gtagRenderer('submit_done')
|
||||||
}
|
}
|
||||||
const handleReadmeItemCheckStatusListChange = (value: number[]) => {
|
const handleReadmeItemCheckStatusListChange = (value: number[]) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<FlyingCompanyLogoList class="flying-company-logo-list" />
|
<FlyingCompanyLogoList class="flying-company-logo-list" />
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
<article>
|
<article>
|
||||||
<h1>👋 BOSS炸弹正在运行</h1>
|
<h1>👋 自动开聊正在运行</h1>
|
||||||
<p>💬 正在为你开聊BOSS,请静候佳音</p>
|
<p>💬 正在为你开聊BOSS,请静候佳音</p>
|
||||||
<p>📱 你可以在<b>手机</b> / <b>平板电脑</b>上,使用BOSS直聘App与为你开聊的BOSS聊天</p>
|
<p>📱 你可以在<b>手机</b> / <b>平板电脑</b>上,使用BOSS直聘App与为你开聊的BOSS聊天</p>
|
||||||
<p>🍀 祝你求职顺利!</p>
|
<p>🍀 祝你求职顺利!</p>
|
||||||
|
|||||||
+10
-9
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<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" />
|
<FlyingCompanyLogoList class="flying-company-logo-list" />
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
<article>
|
<article>
|
||||||
<h1>👋 已读不回提醒器正在运行</h1>
|
<h1>👋 已读不回自动复聊正在运行</h1>
|
||||||
<p>🍀 祝你求职顺利!</p>
|
<p>🍀 祝你求职顺利!</p>
|
||||||
</article>
|
</article>
|
||||||
<el-button :disabled="isStopping" @click="handleStopButtonClick">停止开聊</el-button>
|
<el-button :disabled="isStopping" @click="handleStopButtonClick">停止开聊</el-button>
|
||||||
@@ -23,7 +23,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const handleStopButtonClick = async () => {
|
const handleStopButtonClick = async () => {
|
||||||
gtagRenderer('rnrr_stop_button_clicked')
|
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)
|
const isStopping = ref(false)
|
||||||
@@ -31,23 +31,24 @@ const handleStopping = () => {
|
|||||||
gtagRenderer('rnrr_become_stopping')
|
gtagRenderer('rnrr_become_stopping')
|
||||||
isStopping.value = true
|
isStopping.value = true
|
||||||
}
|
}
|
||||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
ipcRenderer.once('read-no-reply-auto-reminder-stopping', handleStopping)
|
||||||
|
|
||||||
const handleStopped = () => {
|
const handleStopped = () => {
|
||||||
gtagRenderer('rnrr_become_stopped')
|
gtagRenderer('rnrr_become_stopped')
|
||||||
router.replace('/main-layout/ReadNoReplyReminder')
|
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(() => {
|
onUnmounted(() => {
|
||||||
ipcRenderer.removeListener('geek-auto-start-chat-with-boss-stopped', handleStopped)
|
ipcRenderer.removeListener('read-no-reply-auto-reminder-stopped', handleStopped)
|
||||||
ipcRenderer.removeListener('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
ipcRenderer.removeListener('read-no-reply-auto-reminder-stopping', handleStopping)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await electron.ipcRenderer.invoke('run-read-no-reply-auto-reminder')
|
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')) {
|
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
||||||
gtagRenderer('rnrr_cannot_run_for_corrupt')
|
gtagRenderer('rnrr_cannot_run_for_corrupt')
|
||||||
ElMessage.error({
|
ElMessage.error({
|
||||||
@@ -62,7 +63,7 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.geek-auto-start-chat-with-boss__running-status {
|
.read-no-reply-auto-reminder__running-status {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -1,54 +1,29 @@
|
|||||||
<template><RouterView :status="currentStatus" /></template>
|
<template><RouterView :status="currentStatus" /></template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ElMessage } from 'element-plus'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const currentStatus = ref('')
|
const currentStatus = ref('')
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const promise = electron.ipcRenderer.invoke('prepare-run-geek-auto-start-chat-with-boss')
|
switch (route.query.flow) {
|
||||||
const handleLocatingPuppeteerExecutable = () => {
|
case 'geek-auto-start-chat-with-boss': {
|
||||||
currentStatus.value = 'locating-puppeteer-executable'
|
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>
|
</script>
|
||||||
|
|||||||
+1242
-996
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,10 @@
|
|||||||
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
|
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
|
||||||
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
|
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
|
||||||
</template>
|
</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>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="experienceName" label="工作经验" />
|
<ElTableColumn prop="experienceName" label="工作经验" />
|
||||||
@@ -214,12 +218,13 @@ function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const markReasonTopicMap = {
|
const markReasonTopicMap = {
|
||||||
[MarkAsNotSuitReason.BOSS_INACTIVE]: 'Boss不活跃',
|
[MarkAsNotSuitReason.BOSS_INACTIVE]: 'BOSS不活跃',
|
||||||
[MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON]: '手动标记不合适',
|
[MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON]: '手动标记不合适',
|
||||||
[MarkAsNotSuitReason.JOB_NOT_SUIT]: '职位不合适',
|
[MarkAsNotSuitReason.JOB_NOT_SUIT]: '职位不合适',
|
||||||
[MarkAsNotSuitReason.JOB_CITY_NOT_SUIT]: '工作地不合适',
|
[MarkAsNotSuitReason.JOB_CITY_NOT_SUIT]: '工作地不合适',
|
||||||
[MarkAsNotSuitReason.JOB_WORK_EXP_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) {
|
function formatMarkReason(row: VMarkAsNotSuitLog) {
|
||||||
@@ -233,8 +238,8 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
return [
|
return [
|
||||||
extInfo?.bossActiveTimeDesc && `Boss活跃情况:${extInfo.bossActiveTimeDesc}`,
|
extInfo?.bossActiveTimeDesc && `BOSS活跃情况:${extInfo.bossActiveTimeDesc}`,
|
||||||
extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`
|
extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
@@ -247,7 +252,7 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
return [extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`]
|
return [extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
@@ -260,7 +265,7 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
return [extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`]
|
return [extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
@@ -274,11 +279,23 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
|
|||||||
})()
|
})()
|
||||||
return [
|
return [
|
||||||
extInfo?.salaryDesc && `薪资:${extInfo.salaryDesc}`,
|
extInfo?.salaryDesc && `薪资:${extInfo.salaryDesc}`,
|
||||||
extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`
|
extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n')
|
.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: {
|
default: {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,225 +1,248 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="form-wrap">
|
<div class="read-no-reply-reminder__wrap">
|
||||||
<el-form
|
<div class="form-wrap">
|
||||||
ref="formRef"
|
<el-form
|
||||||
:rules="formRules"
|
ref="formRef"
|
||||||
:model="formContent.autoReminder"
|
:rules="formRules"
|
||||||
label-position="top"
|
:model="formContent.autoReminder"
|
||||||
>
|
label-position="top"
|
||||||
<el-form-item label="BOSS直聘 Cookie">
|
>
|
||||||
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
|
<el-form-item>
|
||||||
>编辑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>
|
<div>
|
||||||
<el-tooltip
|
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
|
||||||
effect="light"
|
发送提醒消息前,先按照“自动开聊-职位类型正则”校验正在与BOSS沟通的岗位是否满足期望,校验通过后再提醒
|
||||||
placement="right"
|
</el-checkbox>
|
||||||
:enterable="false"
|
<template v-else>
|
||||||
@show="gtagRenderer('tooltip_show_about_lfr_emotion_figure')"
|
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithExpectJobType">
|
||||||
>
|
发送提醒消息前,先按照“自动开聊-职位类型正则”校验正在与BOSS沟通的岗位是否满足期望,校验通过后再提醒
|
||||||
<template #content>
|
</el-checkbox>
|
||||||
<img block h-100px src="./resources/look-forward-reply-emotion.gif" />
|
<div ml1.5em color-gray>
|
||||||
</template>
|
<div>当前职位类型正则:{{ expectJobTypeRegExpStr?.trim() }}</div>
|
||||||
<el-radio :label="RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION">
|
<template
|
||||||
“[盼回复]” 表情
|
v-if="
|
||||||
</el-radio>
|
formContent.autoReminder.rechatContentSource ===
|
||||||
</el-tooltip>
|
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
|
||||||
<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()
|
|
||||||
}
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
还原默认提示词模板
|
<div>当前简历中填写的期望职位:{{ resumeContent?.expectJob ?? '-' }}</div>
|
||||||
</el-button>
|
<div color-orange>请确保上方二者信息匹配</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-size-12px color-#666">
|
</template>
|
||||||
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入
|
</div>
|
||||||
__REPLACE_REAL_RESUME_HERE__
|
</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>
|
||||||
</div>
|
</template>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item prop="recentMessageQuantityForLlm">
|
</el-form-item>
|
||||||
|
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的BOSS时,将要向BOSS发出:">
|
||||||
|
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
|
||||||
<div>
|
<div>
|
||||||
携带最近
|
<el-tooltip
|
||||||
<el-input-number
|
effect="light"
|
||||||
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
|
placement="right"
|
||||||
class="w-120px"
|
:enterable="false"
|
||||||
:min="8"
|
@show="gtagRenderer('tooltip_show_about_lfr_emotion_figure')"
|
||||||
: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">当所有模型均不可使用时 </span>
|
|
||||||
<el-select
|
|
||||||
v-model="formContent.autoReminder.rechatLlmFallback"
|
|
||||||
class="w200px"
|
|
||||||
label="name"
|
|
||||||
>
|
>
|
||||||
<el-option
|
<template #content>
|
||||||
v-for="option in rechatLlmFallbackOptions"
|
<img block h-100px src="./resources/look-forward-reply-emotion.gif" />
|
||||||
:key="option.value"
|
</template>
|
||||||
:value="option.value"
|
<el-radio :label="RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION">
|
||||||
:label="option.name"
|
“[盼回复]” 表情
|
||||||
/>
|
</el-radio>
|
||||||
</el-select>
|
</el-tooltip>
|
||||||
|
<br />
|
||||||
|
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
|
||||||
|
由大语言模型(根据简历及当前聊天上下文)生成的内容
|
||||||
|
</el-radio>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-radio-group>
|
||||||
</template>
|
</el-form-item>
|
||||||
</div>
|
<div class="ml-30px">
|
||||||
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
|
<template
|
||||||
<el-input-number
|
v-if="
|
||||||
v-model="formContent.autoReminder.throttleIntervalMinutes"
|
formContent.autoReminder.rechatContentSource ===
|
||||||
class="w-150px"
|
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
|
||||||
:min="3"
|
"
|
||||||
:precision="1"
|
>
|
||||||
:step="0.5"
|
<el-form-item class="mb4px">
|
||||||
@blur="handleThrottleIntervalMinutesBlur"
|
<div>
|
||||||
/> 分钟内不多次跟进同一Boss
|
<el-button size="small" type="primary" @click="handleClickEditResume">
|
||||||
</el-form-item>
|
编辑简历
|
||||||
<el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
|
</el-button>
|
||||||
<div>
|
<div class="font-size-12px color-#666">
|
||||||
<div><el-checkbox v-model="enableRechatLimit" /> 启用</div>
|
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
|
||||||
|
</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">当所有模型均不可使用时 </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
|
<el-input-number
|
||||||
v-model="formContent.autoReminder.rechatLimitDay"
|
v-model="formContent.autoReminder.throttleIntervalMinutes"
|
||||||
class="w-150px"
|
class="w-150px"
|
||||||
:min="0"
|
:min="3"
|
||||||
:precision="1"
|
:precision="1"
|
||||||
:step="0.5"
|
:step="0.5"
|
||||||
:disabled="!enableRechatLimit"
|
@blur="handleThrottleIntervalMinutesBlur"
|
||||||
/> 天<br />
|
/> 分钟内不多次跟进同一BOSS
|
||||||
<div v-if="enableRechatLimit">
|
</el-form-item>
|
||||||
不再跟进 (<span class="text-orange">{{ rechatLimitDateString }}</span
|
<el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
|
||||||
>)之前列表中没有进展的聊天
|
<div>
|
||||||
|
<div><el-checkbox v-model="enableRechatLimit" /> 启用</div>
|
||||||
|
<el-input-number
|
||||||
|
v-model="formContent.autoReminder.rechatLimitDay"
|
||||||
|
class="w-150px"
|
||||||
|
:min="0"
|
||||||
|
:precision="1"
|
||||||
|
:step="0.5"
|
||||||
|
:disabled="!enableRechatLimit"
|
||||||
|
/> 天<br />
|
||||||
|
<div v-if="enableRechatLimit">
|
||||||
|
不再跟进 (<span class="text-orange">{{ rechatLimitDateString }}</span
|
||||||
|
>)之前列表中没有进展的聊天
|
||||||
|
</div>
|
||||||
|
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span>)</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span>)</div>
|
</el-form-item>
|
||||||
</div>
|
<el-form-item>
|
||||||
</el-form-item>
|
<el-tooltip
|
||||||
<el-form-item>
|
effect="light"
|
||||||
<el-tooltip
|
placement="bottom-start"
|
||||||
effect="light"
|
@show="gtagRenderer('tooltip_show_about_stop_trace_one_boss')"
|
||||||
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-tooltip>
|
<template #content>
|
||||||
</el-form-item>
|
<ul m0 line-height-1.5em w-300px pl2em>
|
||||||
<el-form-item class="last-form-item">
|
<li>
|
||||||
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
|
请向你不想继续提醒的Boss发送任意消息,发送后立即撤回的这条消息即可。
|
||||||
</el-form-item>
|
<br />
|
||||||
</el-form>
|
<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="running-overlay__wrap"
|
||||||
|
:style="{
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<RunningOverlay
|
||||||
|
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>
|
||||||
|
</RunningOverlay>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -229,11 +252,13 @@ import { dayjs, ElForm, ElMessage, ElMessageBox, ElSelect, ElOption } from 'elem
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
RECHAT_CONTENT_SOURCE,
|
RECHAT_CONTENT_SOURCE,
|
||||||
RECHAT_LLM_FALLBACK
|
RECHAT_LLM_FALLBACK,
|
||||||
|
RUNNING_STATUS_ENUM
|
||||||
} from '../../../../common/enums/auto-start-chat'
|
} from '../../../../common/enums/auto-start-chat'
|
||||||
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
|
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
|
||||||
import mittBus from '../../utils/mitt'
|
import mittBus from '../../utils/mitt'
|
||||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||||
|
import RunningOverlay from '@renderer/features/RunningOverlay/index.vue'
|
||||||
const gtagRenderer = (name, params?: object) => {
|
const gtagRenderer = (name, params?: object) => {
|
||||||
return baseGtagRenderer(name, {
|
return baseGtagRenderer(name, {
|
||||||
scene: 'rnrr-config',
|
scene: 'rnrr-config',
|
||||||
@@ -248,7 +273,8 @@ const formContent = ref({
|
|||||||
rechatContentSource: 1,
|
rechatContentSource: 1,
|
||||||
recentMessageQuantityForLlm: 8,
|
recentMessageQuantityForLlm: 8,
|
||||||
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION,
|
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('')
|
const expectJobTypeRegExpStr = ref('')
|
||||||
async function fetchExpectJobTypeRegExpStr() {
|
const blockCompanyNameRegExpStr = ref('')
|
||||||
|
async function fetchAutoStartChatConfig() {
|
||||||
await electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
|
await electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
|
||||||
expectJobTypeRegExpStr.value = res.config['boss.json']?.expectJobTypeRegExpStr
|
expectJobTypeRegExpStr.value = res.config['boss.json']?.expectJobTypeRegExpStr
|
||||||
|
blockCompanyNameRegExpStr.value = res.config['boss.json']?.blockCompanyNameRegExpStr
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fetchExpectJobTypeRegExpStr()
|
fetchAutoStartChatConfig()
|
||||||
mittBus.on('auto-start-chat-with-boss-config-saved', fetchExpectJobTypeRegExpStr)
|
mittBus.on('auto-start-chat-with-boss-config-saved', fetchAutoStartChatConfig)
|
||||||
onUnmounted(() => {
|
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)
|
const resumeContent = ref(null)
|
||||||
@@ -434,7 +462,8 @@ async function checkIsCanRun() {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
const runRecordId = ref(null)
|
||||||
|
const runningOverlayRef = ref(null)
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
gtagRenderer('run_read_no_reply_reminder_clicked', {
|
gtagRenderer('run_read_no_reply_reminder_clicked', {
|
||||||
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
|
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
|
||||||
@@ -471,10 +500,29 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
gtagRenderer('run_read_no_reply_reminder_launched')
|
gtagRenderer('run_read_no_reply_reminder_launched')
|
||||||
router.replace({
|
|
||||||
path: '/geekAutoStartChatWithBoss/prepareRun',
|
try {
|
||||||
query: { flow: 'read-no-reply-reminder' }
|
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() {
|
function handleThrottleIntervalMinutesBlur() {
|
||||||
if (formContent.value.autoReminder.throttleIntervalMinutes < 3) {
|
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())
|
const currentStamp = ref(new Date())
|
||||||
let timer = 0
|
let timer = 0
|
||||||
function updateCurrentStamp() {
|
function updateCurrentStamp() {
|
||||||
@@ -515,15 +558,6 @@ const rechatLimitDateString = computed(() => {
|
|||||||
).format('YYYY-MM-DD HH:mm:ss')
|
).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 () => {
|
const handleClickEditResume = async () => {
|
||||||
gtagRenderer('edit_resume_clicked')
|
gtagRenderer('edit_resume_clicked')
|
||||||
try {
|
try {
|
||||||
@@ -545,7 +579,7 @@ const rechatLlmFallbackOptions = [
|
|||||||
value: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
value: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '退出已读不回提醒器',
|
name: '退出已读不回自动复聊',
|
||||||
value: RECHAT_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
|
value: RECHAT_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -559,24 +593,54 @@ async function handleTestEffectClicked() {
|
|||||||
autoReminderConfig: JSON.parse(JSON.stringify(formContent.value.autoReminder))
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss">
|
||||||
.form-wrap {
|
.read-no-reply-reminder__wrap {
|
||||||
max-height: 100vh;
|
position: relative;
|
||||||
overflow: auto;
|
.form-wrap {
|
||||||
padding-left: 20px;
|
max-height: 100vh;
|
||||||
padding-right: 20px;
|
overflow: auto;
|
||||||
:deep(.el-form) {
|
padding-left: 20px;
|
||||||
margin: 0 auto;
|
padding-right: 20px;
|
||||||
max-width: 1000px;
|
.el-form {
|
||||||
padding-top: 8px;
|
margin: 0 auto;
|
||||||
}
|
max-width: 1000px;
|
||||||
.last-form-item {
|
padding-top: 8px;
|
||||||
:deep(.el-form-item__content) {
|
|
||||||
margin-top: 0px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
|
.last-form-item {
|
||||||
|
.el-form-item__content {
|
||||||
|
margin-top: 0px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.running-overlay__wrap {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<TaskManagerList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import TaskManagerList from '../../features/TaskManager/List.vue'
|
||||||
|
</script>
|
||||||
@@ -1,99 +1,155 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h100vh">
|
<div class="flex h100vh">
|
||||||
<div class="flex flex-col min-w200px w200px pt30px pl30px aside-nav of-hidden">
|
<div class="flex flex-col min-w200px w200px pt30px pl30px aside-nav of-hidden">
|
||||||
<div class="nav-list flex-1 of-auto">
|
<div class="nav-list flex-1 of-auto pl20px ml--20px">
|
||||||
<RouterLink to="./GeekAutoStartChatWithBoss">
|
<RouterLink v-show="false" to="./TaskManager">任务管理</RouterLink>
|
||||||
Boss炸弹
|
<div class="group-item">
|
||||||
<el-tooltip
|
<div class="group-title">BOSS直聘</div>
|
||||||
placement="right"
|
<div flex flex-col class="link-list">
|
||||||
:enterable="false"
|
<RouterLink to="./GeekAutoStartChatWithBoss">
|
||||||
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
|
自动开聊
|
||||||
>
|
<el-tooltip
|
||||||
<template #content>
|
placement="right"
|
||||||
<div w-480px>
|
:enterable="false"
|
||||||
<div>扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。</div>
|
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
|
||||||
<br />
|
>
|
||||||
<div>匹配步骤</div>
|
<template #content>
|
||||||
<ol m0 pl2em>
|
<div w-480px>
|
||||||
<li>
|
<div>扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的BOSS。</div>
|
||||||
按照公司名称查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
<br />
|
||||||
</li>
|
<div>匹配步骤</div>
|
||||||
<li>
|
<ol m0 pl2em>
|
||||||
检查Boss活跃度
|
|
||||||
<ul pl2em>
|
|
||||||
<li>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
<br />
|
||||||
<li>
|
<div>发送内容</div>
|
||||||
对职位名称、职位类型、职位描述进行匹配
|
<ul m0 pl2em>
|
||||||
<ul pl2em>
|
<li>“[盼回复]”表情</li>
|
||||||
<li>如果匹配则自动点击开聊按钮</li>
|
<li>由大语言模型(根据简历及当前聊天上下文)生成的内容</li>
|
||||||
<li>
|
|
||||||
不匹配则标记这个职位为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</div>
|
||||||
</ol>
|
</template>
|
||||||
<br />
|
<QuestionFilled w-1em h-1em mr10px />
|
||||||
<div>异常情况</div>
|
</el-tooltip>
|
||||||
<ol m0 pl2em>
|
</RouterLink>
|
||||||
<li>
|
<a href="javascript:void(0)" @click="handleClickLaunchBossLogin">
|
||||||
当前页面筛选条件下,如果没有更多职位,则自动切换备选筛选条件,以获取更多新职位
|
编辑登录凭据<TopRight w-1em h-1em mr10px />
|
||||||
</li>
|
</a>
|
||||||
<li>
|
<a href="javascript:void(0)" @click="handleLaunchBossSite">
|
||||||
如当天开聊次数用完,本程序会暂停运行60分钟,之后尝试继续重新运行;如重新运行时间已在第二天,则将会继续开聊
|
手动逛<TopRight w-1em h-1em mr10px />
|
||||||
</li>
|
</a>
|
||||||
</ol>
|
</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>
|
</div>
|
||||||
</template>
|
</a>
|
||||||
<QuestionFilled w-1em h-1em mr10px />
|
</div>
|
||||||
</el-tooltip>
|
</div>
|
||||||
</RouterLink>
|
<hr class="group-divider" />
|
||||||
<RouterLink to="./ReadNoReplyReminder">
|
<div class="group-item">
|
||||||
已读不回提醒器
|
<div class="group-title">运行数据</div>
|
||||||
<el-tooltip
|
<div flex flex-col class="link-list">
|
||||||
placement="right"
|
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
|
||||||
:enterable="false"
|
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
|
||||||
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
|
<RouterLink to="./JobLibrary">职位库</RouterLink>
|
||||||
>
|
<RouterLink to="./BossLibrary">BOSS库</RouterLink>
|
||||||
<template #content>
|
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
|
||||||
<div w-480px>
|
</div>
|
||||||
<div>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-16px pb-16px flex-0 font-size-12px">
|
<div class="pt-16px pb-16px flex-0 font-size-12px">
|
||||||
<div v-if="updateStore.availableNewRelease" mb16px>
|
<div v-if="updateStore.availableNewRelease" mb16px>
|
||||||
@@ -136,56 +192,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RouterView v-slot="{ Component }" class="flex-1">
|
<div class="router-view-wrap">
|
||||||
<KeepAlive>
|
<RouterView v-slot="{ Component }" class="flex-1 of-hidden">
|
||||||
<component :is="Component" />
|
<KeepAlive>
|
||||||
</KeepAlive>
|
<component :is="Component" />
|
||||||
</RouterView>
|
</KeepAlive>
|
||||||
|
</RouterView>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
|
import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
|
||||||
import useBuildInfo from '@renderer/hooks/useBuildInfo'
|
import useBuildInfo from '@renderer/hooks/useBuildInfo'
|
||||||
import { debounce } from 'lodash-es'
|
import { debounce } from 'lodash'
|
||||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
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()
|
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
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const { buildInfo } = useBuildInfo()
|
const { buildInfo } = useBuildInfo()
|
||||||
const handleFeedbackClick = () => {
|
const handleFeedbackClick = () => {
|
||||||
@@ -217,34 +243,85 @@ function handleViewNewReleaseClick() {
|
|||||||
gtagRenderer('click_view_release_form_nav')
|
gtagRenderer('click_view_release_form_nav')
|
||||||
electron.ipcRenderer.send('open-external-link', updateStore.availableNewRelease!.releasePageUrl)
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.aside-nav {
|
.aside-nav {
|
||||||
background-image: linear-gradient(45deg, #eaf4f1, #dcf6f2);
|
background-image: linear-gradient(45deg, #eaf4f1, #dcf6f2);
|
||||||
.nav-list {
|
.nav-list {
|
||||||
> a {
|
hr.group-divider {
|
||||||
display: flex;
|
width: 100%;
|
||||||
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 {
|
|
||||||
border: 0 solid;
|
border: 0 solid;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: #b3c8c3;
|
background-color: #b3c8c3;
|
||||||
margin-top: 0;
|
margin-top: 4px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 4px;
|
||||||
margin-right: 0;
|
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,
|
.feedback-button-area,
|
||||||
.update-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>
|
</style>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 232 KiB |
@@ -14,7 +14,28 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/cookieAssistant',
|
path: '/cookieAssistant',
|
||||||
component: () => import('@renderer/page/CookieAssistant/index.vue'),
|
component: () => import('@renderer/page/CookieAssistant/index.vue'),
|
||||||
meta: {
|
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',
|
path: '/readNoReplyReminderLlmMock',
|
||||||
component: () => import('@renderer/page/ReadNoReplyReminderLlmMock/index.vue'),
|
component: () => import('@renderer/page/ReadNoReplyReminderLlmMock/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '已读不回提醒器 大语言模型测试'
|
title: '已读不回自动复聊 大语言模型测试'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,18 +64,25 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import('@renderer/page/MainLayout/index.vue'),
|
component: () => import('@renderer/page/MainLayout/index.vue'),
|
||||||
redirect: '/main-layout/GeekAutoStartChatWithBoss',
|
redirect: '/main-layout/GeekAutoStartChatWithBoss',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: 'taskManager',
|
||||||
|
component: () => import('@renderer/page/MainLayout/TaskManager.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '任务管理'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'GeekAutoStartChatWithBoss',
|
path: 'GeekAutoStartChatWithBoss',
|
||||||
component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss/index.vue'),
|
component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'BOSS炸弹'
|
title: '自动开聊'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'ReadNoReplyReminder',
|
path: 'ReadNoReplyReminder',
|
||||||
component: () => import('@renderer/page/MainLayout/ReadNoReplyReminder.vue'),
|
component: () => import('@renderer/page/MainLayout/ReadNoReplyReminder.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '已读不回提醒器'
|
title: '已读不回自动复聊'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -82,7 +110,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: 'BossLibrary',
|
path: 'BossLibrary',
|
||||||
component: () => import('@renderer/page/MainLayout/BossLibrary.vue'),
|
component: () => import('@renderer/page/MainLayout/BossLibrary.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Boss库'
|
title: 'BOSS库'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,14 +130,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: 'prepareRun',
|
path: 'prepareRun',
|
||||||
component: () => import('@renderer/page/GeekAutoStartChatWithBoss/PrepareRun.vue'),
|
component: () => import('@renderer/page/GeekAutoStartChatWithBoss/PrepareRun.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'BOSS炸弹 正在预热'
|
title: '自动开聊 正在预热'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'runningStatus',
|
path: 'runningStatus',
|
||||||
component: () => import('@renderer/page/GeekAutoStartChatWithBoss/RunningStatus.vue'),
|
component: () => import('@renderer/page/GeekAutoStartChatWithBoss/RunningStatus.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'BOSS炸弹 正在为你开聊BOSS'
|
title: '自动开聊 正在为你开聊BOSS'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -119,7 +147,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
'@renderer/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue'
|
'@renderer/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue'
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
title: '已读不回提醒器 正在为你开聊BOSS'
|
title: '已读不回自动复聊 正在为你开聊BOSS'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -128,17 +156,8 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
component: BootstrapSplash,
|
component: BootstrapSplash,
|
||||||
meta: {
|
meta: {
|
||||||
title: '薪想事成'
|
title: '你的职场大机密'
|
||||||
},
|
}
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '/downloadingDependencies',
|
|
||||||
component: () => import('@renderer/page/BootstrapSplash/page/DownloadingDependencies.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '正在下载核心组件'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { NewReleaseInfo } from '../../../common/types/update'
|
import { NewReleaseInfo } from '../../../common/types/update'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { throttle } from 'lodash'
|
||||||
|
|
||||||
export const useUpdateStore = defineStore('update', () => {
|
export const useUpdateStore = defineStore('update', () => {
|
||||||
const availableNewRelease = ref<NewReleaseInfo | null>(null)
|
const availableNewRelease = ref<NewReleaseInfo | null>(null)
|
||||||
@@ -18,3 +19,16 @@ export const useUpdateStore = defineStore('update', () => {
|
|||||||
setInterval(checkUpdate, 30 * 30 * 1000)
|
setInterval(checkUpdate, 30 * 30 * 1000)
|
||||||
return { availableNewRelease }
|
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 }
|
||||||
|
})
|
||||||
|
|||||||
@@ -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) {
|
||||||
Generated
+773
-531
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user