mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-07-11 23:41:51 +08:00
Compare commits
262 Commits
ui-v0.4.0
...
feature/bl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e4f044909 | ||
|
|
a1f8b6c6f0 | ||
|
|
c0326605a9 | ||
|
|
0460cb331d | ||
|
|
85acb599d5 | ||
|
|
f88e295138 | ||
|
|
81ba666e27 | ||
|
|
e33d77c57c | ||
|
|
1d8e99303d | ||
|
|
baef48752f | ||
|
|
46c0bcba42 | ||
|
|
6ab40f759a | ||
|
|
bfb4fa6e8d | ||
|
|
37ad84ff46 | ||
|
|
3077a0f68f | ||
|
|
68170e8b60 | ||
|
|
c298904d25 | ||
|
|
3eeea2e5c7 | ||
|
|
e746bf70e9 | ||
|
|
8cc94baa18 | ||
|
|
3f2b709097 | ||
|
|
ebf512c86a | ||
|
|
9f97622da9 | ||
|
|
e2ed83c421 | ||
|
|
8105f9933a | ||
|
|
f2cc87d660 | ||
|
|
dcf4706eb6 | ||
|
|
52094a4ce3 | ||
|
|
1d5fdb9d54 | ||
|
|
9fc16106cd | ||
|
|
aff310b51a | ||
|
|
3a9dcf13c7 | ||
|
|
27652824d9 | ||
|
|
fb7f2bd0b1 | ||
|
|
a70a316800 | ||
|
|
4d1c1c4f4b | ||
|
|
c663b1be1c | ||
|
|
f04d3843be | ||
|
|
0e60819df1 | ||
|
|
5d46607e0c | ||
|
|
9889182082 | ||
|
|
8a45c1bf04 | ||
|
|
838d4e2da1 | ||
|
|
097ada6bda | ||
|
|
a92b9ce040 | ||
|
|
aa6527432d | ||
|
|
64f9ddfb76 | ||
|
|
6b131e708c | ||
|
|
1e9f436633 | ||
|
|
0c815f2557 | ||
|
|
b8753597da | ||
|
|
98300ddf66 | ||
|
|
962dcb1125 | ||
|
|
a9823f1e24 | ||
|
|
57eb09ed49 | ||
|
|
a8454cbaac | ||
|
|
c5789679ba | ||
|
|
204e15b5d8 | ||
|
|
60dee6f435 | ||
|
|
5ab7fc2ee2 | ||
|
|
afe8a820c1 | ||
|
|
c23d590052 | ||
|
|
0b50d2767a | ||
|
|
a1f6dceedb | ||
|
|
f09db784fa | ||
|
|
0df3174ad9 | ||
|
|
ce32dc4c2f | ||
|
|
351f30b824 | ||
|
|
754f942c86 | ||
|
|
5063f9c2aa | ||
|
|
9639151fdd | ||
|
|
4cf6a7e36f | ||
|
|
a05c73cbdf | ||
|
|
199a472ec3 | ||
|
|
5634ac0260 | ||
|
|
55a04bb1ad | ||
|
|
492538e663 | ||
|
|
6f02160f28 | ||
|
|
860847963b | ||
|
|
69ec879967 | ||
|
|
76985b8c7c | ||
|
|
45397bcc74 | ||
|
|
8b41f479cf | ||
|
|
ddc8a81c7c | ||
|
|
ca37e258c6 | ||
|
|
454ee421bf | ||
|
|
69bd774640 | ||
|
|
20f081a245 | ||
|
|
252869576b | ||
|
|
98ab2acf50 | ||
|
|
dbc127be06 | ||
|
|
82f19d772d | ||
|
|
94de507c9a | ||
|
|
c1faecaaed | ||
|
|
880557c1ad | ||
|
|
3b318262a4 | ||
|
|
f9786d5d82 | ||
|
|
b583951cb5 | ||
|
|
d95abd3ad1 | ||
|
|
2e9d3af276 | ||
|
|
fbeb01ab48 | ||
|
|
6db6ef4972 | ||
|
|
7b3390745a | ||
|
|
b2b1cecfa9 | ||
|
|
61d7d968a1 | ||
|
|
c18930f76e | ||
|
|
fc13742d52 | ||
|
|
af05a5555e | ||
|
|
bc57a07a0a | ||
|
|
265f2f89e5 | ||
|
|
206d50ca61 | ||
|
|
9324f20ad3 | ||
|
|
632a11b105 | ||
|
|
933caaff2a | ||
|
|
8739a70277 | ||
|
|
18af99f10e | ||
|
|
81e40230d1 | ||
|
|
b730901775 | ||
|
|
f4ca2ebbbd | ||
|
|
418ca66a30 | ||
|
|
0928b402fb | ||
|
|
373e45dee8 | ||
|
|
8bbfa5b41e | ||
|
|
aa410cfc60 | ||
|
|
e986fddbfb | ||
|
|
cf7a0d3902 | ||
|
|
c95349c037 | ||
|
|
8197782102 | ||
|
|
f17d626ea0 | ||
|
|
2b4b620977 | ||
|
|
03e3c3661f | ||
|
|
5d787fdd50 | ||
|
|
b68ff63b6b | ||
|
|
2aee76307f | ||
|
|
e2040caa38 | ||
|
|
fba45e420f | ||
|
|
fe629eaa22 | ||
|
|
056ec7a0a6 | ||
|
|
f27ca4145a | ||
|
|
cd09210017 | ||
|
|
aec72c28f0 | ||
|
|
69cf5fd34f | ||
|
|
5f47e47448 | ||
|
|
4e9b44d04c | ||
|
|
d206d45242 | ||
|
|
0aef2af350 | ||
|
|
c3dc75b503 | ||
|
|
7a1ae48aa7 | ||
|
|
e5225006cc | ||
|
|
fdf2c61620 | ||
|
|
1f140121fb | ||
|
|
57cf631b86 | ||
|
|
36aef1c16d | ||
|
|
d21972020e | ||
|
|
51ca5dd71d | ||
|
|
e7d5ef382e | ||
|
|
59a36dc7bd | ||
|
|
73b57e21e4 | ||
|
|
6f0d98653c | ||
|
|
3bf6d4c358 | ||
|
|
2c7ecafee2 | ||
|
|
f88a5a2889 | ||
|
|
ac19ae3832 | ||
|
|
a25b047f99 | ||
|
|
42fc66a0f4 | ||
|
|
203fbba9a0 | ||
|
|
b821bc500c | ||
|
|
0032a356e5 | ||
|
|
b9c4976291 | ||
|
|
558891ad9f | ||
|
|
d2da9f5a7d | ||
|
|
71068be21f | ||
|
|
2b0cf2a2fa | ||
|
|
ceaf68e4eb | ||
|
|
10cc248784 | ||
|
|
1d155e498a | ||
|
|
d3e086f321 | ||
|
|
5f97b3ccec | ||
|
|
7dd0c675d5 | ||
|
|
c1978417da | ||
|
|
e0670a9f30 | ||
|
|
b05031be8a | ||
|
|
7b10e97fa8 | ||
|
|
e867acb39d | ||
|
|
b31224a81c | ||
|
|
ddb0cb97d7 | ||
|
|
f338dcffc8 | ||
|
|
84a79c9bae | ||
|
|
44b0e44e2e | ||
|
|
08028f37dd | ||
|
|
a0b09ebae4 | ||
|
|
f078500c14 | ||
|
|
b89d6e264d | ||
|
|
78ddc949f4 | ||
|
|
a208ffe7d4 | ||
|
|
bc70031e2b | ||
|
|
cc821f46f5 | ||
|
|
f668b3e5f5 | ||
|
|
d1441b5761 | ||
|
|
e88b4da53a | ||
|
|
d218778690 | ||
|
|
f903ef9175 | ||
|
|
d114685116 | ||
|
|
bb30329d6c | ||
|
|
3683ead1d4 | ||
|
|
cac3e480e5 | ||
|
|
52163342c3 | ||
|
|
8efce15fd5 | ||
|
|
d5dcdb4bd1 | ||
|
|
98b99c5d14 | ||
|
|
d4fa72a28e | ||
|
|
b024c6b517 | ||
|
|
9b64f05cb6 | ||
|
|
eefb25a3da | ||
|
|
2dad39c8e4 | ||
|
|
699bdd2641 | ||
|
|
48f1232f7d | ||
|
|
2409699ccf | ||
|
|
64c007066e | ||
|
|
bd503eb268 | ||
|
|
8dc8ca825d | ||
|
|
8e8fb6d331 | ||
|
|
2f3a557fb5 | ||
|
|
780cd73e46 | ||
|
|
6c05128e89 | ||
|
|
e235ae099b | ||
|
|
d479687b8b | ||
|
|
4627b4218e | ||
|
|
b64f411e4a | ||
|
|
12ff9dc4ba | ||
|
|
959c32eb63 | ||
|
|
ce38ecea55 | ||
|
|
340f8946b9 | ||
|
|
59002f73e9 | ||
|
|
5626cf6721 | ||
|
|
fe1c85b38d | ||
|
|
24018f63b5 | ||
|
|
5effcc27ab | ||
|
|
aee6340c6a | ||
|
|
03df5f8d5a | ||
|
|
7d3a0e80d4 | ||
|
|
10f1f150bf | ||
|
|
64249593b8 | ||
|
|
217b2bbf2a | ||
|
|
02667fe2f8 | ||
|
|
5539842392 | ||
|
|
b57e646147 | ||
|
|
6982f6028d | ||
|
|
f4cd4b884b | ||
|
|
61c28ae164 | ||
|
|
ece7fc61e3 | ||
|
|
97793adb0f | ||
|
|
b5c3116c01 | ||
|
|
10cd1cf11e | ||
|
|
7d2e60dfd0 | ||
|
|
6986fbdc3c | ||
|
|
5174da2c6c | ||
|
|
043d782a05 | ||
|
|
67e4b94f9e | ||
|
|
45d14e70fb | ||
|
|
44202b4685 | ||
|
|
00c699d1a6 |
7
.github/workflows/release-ui.yml
vendored
7
.github/workflows/release-ui.yml
vendored
@@ -60,9 +60,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- os: macos-13 # for x64 build
|
||||
- os: macos-15-intel # for x64 build
|
||||
arch: x64
|
||||
- os: macos-14 # for arm64 build
|
||||
- os: macos-15 # for arm64 build
|
||||
arch: arm64
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 'true'
|
||||
@@ -149,9 +149,10 @@ jobs:
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -llR geekgeekrun-ui@${{ github.sha }}
|
||||
|
||||
- name: Create release
|
||||
- name: Create release for public
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
repo: geekgeekrun
|
||||
prerelease: true
|
||||
allowUpdates: true
|
||||
artifacts: geekgeekrun-ui@${{ github.sha }}/*
|
||||
|
||||
137
README.md
137
README.md
@@ -1,6 +1,8 @@
|
||||
# 牛人快跑 - GeekGeekRun
|
||||
|
||||
一款可以帮助你在Boss直聘上**自动批量开聊Boss**的脚本,基于Puppeteer。
|
||||
**->维护者正在疯狂求职中<-**
|
||||
|
||||
一款可以帮助你在BOSS直聘上**自动批量开聊BOSS**的脚本,基于Puppeteer。
|
||||
|
||||
与每一位牛人站在一起
|
||||
|
||||
@@ -9,38 +11,52 @@
|
||||
|
||||
各行各业,无论你是小白还是大佬,都能通过几步简单的配置,快速开始求职!
|
||||
|
||||
## TO 有求职之外其他目的朋友
|
||||
本程序的目标,是帮助求职者尽快寻得一个能让他为之**挥洒汗水**、**努力拼搏**的**事业**。如果有其它目的,请勿下载使用。
|
||||
|
||||
## 程序有哪些功能?运行逻辑是什么?怎样使用?
|
||||
|
||||
### Boss炸弹
|
||||
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。
|
||||
### 自动开聊
|
||||
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的BOSS。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
|
||||
|
||||
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ebf5c51c-f52c-4426-a60a-97916f663c6a" />
|
||||
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
|
||||
|
||||
|
||||
#### 匹配步骤
|
||||
1. 按照公司名称查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
||||
2. 检查Boss活跃度
|
||||
- 如果Boss活跃度为本月活跃或更往前的时间,则会把职位标记为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
||||
3. 对职位名称、职位类型、职位描述进行匹配
|
||||
- 如果匹配则自动点击开聊按钮
|
||||
- 不匹配则标记这个职位为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
||||
1. 选择职位来源、职位筛选条件
|
||||
<img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
|
||||
|
||||
2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
||||
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、BOSS活跃度进行匹配
|
||||
- 如果可以匹配,则自动点击开聊按钮
|
||||
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
|
||||
- 首先查看是否命中策略为“在BOSS直聘上标记不合适”的配置,如果命中则在BOSS直聘上点击相关UI,标记不合适,一段时间内你将不会在BOSS上看到这个职位,且将会推荐新职位置换这个职位(对于“推荐列表中的职位”、“通过搜索找到的职位”,此项会回落到“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”)
|
||||
- 然后查看是否命中策略为“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”的配置,如果命中则仅在本地数据库中记录,且本次运行将无视这个职位
|
||||
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
|
||||
|
||||
#### 异常情况
|
||||
- 当前页面筛选条件下,如果没有更多职位,则自动切换备选筛选条件,以获取更多新职位
|
||||
- 当前页面筛选条件下,如果没有更多可投递职位,则自动切换筛选条件,以获取更多新职位;如果所有筛选条件搜索完没有更多可投递职位,则切换其它职位来源继续查找更多新职位;如果所有职位来源筛选完没有更多可投递职位,则等候一段时间后,会回到第一个职位来源的第一个筛选条件,从头开始查找职位
|
||||
- 如当天开聊次数用完,本程序会暂停运行60分钟,之后尝试继续重新运行;如重新运行时间已在第二天,则将会继续开聊
|
||||
|
||||
### 已读不回提醒器
|
||||
Boss不明原因已读不回?简历就是投不出去?
|
||||
#### 运行效果
|
||||
|
||||
已读不回提醒器,有事没事提醒一下已读不回的 Ta,助力把握每次机会
|
||||
> BOSS直聘界面已改版,如下是旧版BOSS直聘界面
|
||||
|
||||
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
|
||||
|
||||
|
||||
### 已读不回自动复聊
|
||||
BOSS不明原因已读不回?简历就是投不出去?
|
||||
|
||||
已读不回自动复聊,有事没事提醒一下已读不回的 Ta,助力沟通双向奔赴
|
||||
|
||||
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/51a43b0f-118d-4b0a-957c-40a5367e29e4" />
|
||||
|
||||
|
||||
#### 匹配逻辑
|
||||
1. 在聊天列表中查找对你消息已读不回的Boss,再发一条消息,多次复聊;同时:
|
||||
1. 在聊天列表中查找对你消息已读不回的BOSS,再发一条消息,多次复聊;同时:
|
||||
- 如果设置了“跟进时限”,那么在这个时间之前活跃的聊天将不会被检查
|
||||
- 如果设置了“跟进间隔”,且再次检查时发现Boss已读不回,且距离上次提醒时间间隔小于这个时间,那么聊天将暂时不会跟进,直到下次检查时距离上次提醒时间间隔大于这个时间
|
||||
- 如果设置了“跟进间隔”,且再次检查时发现BOSS已读不回,且距离上次提醒时间间隔小于这个时间,那么聊天将暂时不会跟进,直到下次检查时距离上次提醒时间间隔大于这个时间
|
||||
|
||||
#### 发送内容
|
||||
以下二者之一:
|
||||
@@ -48,8 +64,8 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
2. 由大语言模型(根据简历及当前聊天上下文)生成的内容
|
||||
|
||||
## 辅助功能介绍
|
||||
### Boss 登录助手
|
||||
帮你用十分简单的方式,登录到 Boss 直聘,以进行自动开聊、已读不回提醒,避免和乱七八糟的技术名词(类似:Cookie、登录凭证、JSON……)打交道
|
||||
### BOSS 登录助手
|
||||
帮你用十分简单的方式,登录到 BOSS 直聘,以进行自动开聊、已读不回提醒,避免和乱七八糟的技术名词(类似:Cookie、登录凭证、JSON……)打交道
|
||||
|
||||
<img width="1680" alt="image" src="https://github.com/user-attachments/assets/b5d02385-f1c0-4045-b01a-66c5f747fec5" />
|
||||
|
||||
@@ -60,20 +76,20 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
<img width="576" alt="image" src="https://github.com/user-attachments/assets/ef91642f-b0b0-438f-8c66-e650f0de139b" />
|
||||
|
||||
|
||||
### 已读不回提醒器提示词模板编辑
|
||||
### 已读不回自动复聊提示词模板编辑
|
||||
对生成的提醒消息不够满意?您不妨可以自己进行编辑提示词,来获得您期望的结果
|
||||
|
||||
<img width="1139" alt="image" src="https://github.com/user-attachments/assets/db956573-61c0-44d4-b588-5c04fe941f84" />
|
||||
|
||||
|
||||
### 已读不回提醒器提醒过程模拟
|
||||
### 已读不回自动复聊提醒过程模拟
|
||||
想要测试配置的某一款模型可不可用?想测试使用当前配置生成的内容效果如何?那就来这里试试吧
|
||||
|
||||
<img width="600" alt="image" src="https://github.com/user-attachments/assets/3b2776bd-ae5b-48d1-a01c-0819509d96a1" />
|
||||
|
||||
|
||||
### 配置模板选项
|
||||
初次见面,不会配置本程序?当心配置出现问题?本程序为一些关键的功能配置(例如:Boss炸弹中的期望公司设置、职位详情筛选设置;大语言模型设置)加入了了配置模板,让本程序更易用,助你轻松完成配置,开始求职。不妨来试试吧
|
||||
初次见面,不会配置本程序?当心配置出现问题?本程序为一些关键的功能配置(例如:自动开聊中的期望公司设置、职位详情筛选设置;大语言模型设置)加入了配置模板,让本程序更易用,助你轻松完成配置,开始求职。不妨来试试吧
|
||||
|
||||
<img width="576" alt="image" src="https://github.com/user-attachments/assets/2cccfece-17e7-4cc2-bf33-67f0ce500625" />
|
||||
|
||||
@@ -108,46 +124,47 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
```
|
||||
1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置
|
||||
|
||||
## 关于软件不能正常启动、不能按预期运行的一些说明
|
||||
本程序一直都在不断地更新迭代中,包括新功能的增加以及Bug的修复。
|
||||
|
||||
然而作者本人只有 1台 macOS (Apple Silicon)设备、1台 Windows 设备,且只有作者1人自测(白天工作,晚上熬夜开发、测试),测试条件不是太好(人力不足、设备不足),测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时,即认为可以准出、发版。
|
||||
|
||||
所以不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看,问题也会尝试进行修复。
|
||||
|
||||
如果部分朋友超级喜欢写代码,或者本身工作就是写代码,可以帮忙下载 [Debugtron](https://github.com/pd4d10/debugtron/releases/tag/v1.0.0-alpha.0) 来调试本程序,查看反馈本程序的日志(如下图)。
|
||||
|
||||

|
||||
|
||||
在此先谢过了~
|
||||
|
||||
## 为什么要写这个程序?
|
||||
### 个人而言
|
||||
以我为例,我真的求职求累了
|
||||
|
||||
2023年初~2025年初,这段时间
|
||||
- 求职经历:离职求职5个月,在职求职3个月;曾面试66个岗位,各种面试(一面、二面、三面、HR面)加起来134次;Offer 寥寥无几
|
||||
- 被动失业经历:1 次裁员,1 次卡试用期
|
||||
- 目前状态:有工作(但愿明天也有工作)
|
||||
|
||||
如此多的经历,已经让我成为了一位资深Boss用户
|
||||
|
||||
2023年,处于Gap期时,一天差不多可以有8小时浪费在Boss上挑选工作、开聊Boss上,这让我内心经常会很心累,时常陷入内耗,有种被全世界放弃的感觉
|
||||
|
||||
当时就有了想法,要让求职自动化。虽然后续也有新工作,但同工不同酬、卡试用期等情况,我还是有一些感受的。由此,便有了这个项目
|
||||
|
||||
我把之前的求职经验,通过这个程序表现了出来,假设你也在看机会,或许我的求职经验可以帮到你,让你少走弯路、减少内耗,愿你也能找到一份更好的工作
|
||||
|
||||
本程序完全公益、免费、开源。如需使用大模型功能,请自行到你喜欢的平台(例如:DeepSeek、阿里云、火山引擎、OpenAI)开通、充值,并在本程序中配置
|
||||
|
||||
### 大环境而言
|
||||
个人感受,大环境真的差,对于求职者相当不友好
|
||||
|
||||
一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场
|
||||
|
||||
当然,如果运气好:
|
||||
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个烂 Offer
|
||||
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了烂 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
|
||||
|
||||
为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧
|
||||
|
||||
因此,我编写了Boss炸弹,可以帮你尽可能多地将当日开聊机会用完;也编写了已读不回提醒器,帮你戳一戳列表里读了消息但无回应的 Boss
|
||||
|
||||
### 求职平台(Boss直聘)而言
|
||||
根据日常使用经验,Boss直聘在已经开聊很多职位的情况下,经常会推荐:
|
||||
根据日常使用求职平台(BOSS直聘)的经验,在已经开聊很多职位的情况下,经常会推荐:
|
||||
1. 一些长时间不活跃的“僵尸”职位 - 活跃信息默认隐藏,需要点开职位详情才能看到
|
||||
2. 牛头不对马嘴,不符合求职期望的职位
|
||||
|
||||
人工筛选这些职位会有很大的心智负担,真的会吐……
|
||||
|
||||
因此,我在Boss炸弹代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位
|
||||
因此,我在自动开聊代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位。
|
||||
|
||||
## 使用必读及免责声明
|
||||
如下是使用必读及免责声明,请您务必逐条阅读;本程序首次启动时,您还会再看到一次;若您不接受如下提到的任何内容,请不要使用本程序。
|
||||
- 本程序从某种程度上说属于辅助工具,与《<a href="https://about.zhipin.com/agreement/?id=registerprotocol_30" rel="noreferer noopener" target="blank">BOSS直聘用户协议</a>》(2023年3月版)相关条款相违背,您在注册BOSS直聘时已签署过这一条款;根据该条款`七、用户的平台使用义务`、`八、违约责任` 章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于**账号被强制退出登录**、**账号被限制使用**、**账号被封禁**等对您不利的风险;因此使用本程序即意味着**您愿意接受以上风险**,且如果相关风险发生,您需要自行承担相关后果,**本程序概不负责** 。
|
||||
- 本程序需要存储您的登录凭据,即Cookie,来模拟您在BOSS直聘上开聊BOSS的行为;本程序仅会把您的Cookie存储在本地,并在您访问BOSS直聘时将其传输到BOSS直聘,**不会泄露给第三方**,也不会进行除自动开聊BOSS以外的行为;**请勿向他人泄漏您的Cookie**。
|
||||
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的BOSS直聘账号**进行求职。
|
||||
- 本程序原理是模拟用户在BOSS直聘网页上,寻找关键元素并进行点击操作;BOSS直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
||||
- 您所在公司可能会对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;上级/HRBP团队可能会从 IT 团队处获取到监控数据,从而了解团队成员离职倾向。如果您不希望上级/HRBP团队了解到您正在求职,建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
|
||||
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及上级/HRBP报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**。
|
||||
- 本程序**没有内置任何付费功能**,**下载**、**使用**是**免费**的,任何人可以**免费获得**、**免费使用**。**作者没有利用本程序赚到过任何收入**。如果您是从GitHub以外的地方付费后“购买”的本程序,或您被提示“必须付费后才能使用本程序”,那**您大概率被骗了**,或者**您下载到了本程序修改版**。**本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方**。
|
||||
- 本程序**不对您的求职过程与结果负责**,为您开聊的职位均在BOSS直聘上发布,职位信息真实性由BOSS直聘负责;请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
|
||||
- 请在BOSS直聘上自行**屏蔽您不期望投递的公司**;如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃,请**在BOSS直聘上屏蔽当前公司及与之关联的公司**。
|
||||
- 本程序经历过了多次测试,理论上来说大部分情况下可以正常运行,但可能也会出现测试用例覆盖不到位,导致程序不按预期运行的情况;如果您有顾虑,建议通过VMware Workstation/Fusion、Oracle VirtualBox、Microsoft Hyper-V等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
|
||||
|
||||
## 想联系作者?想反馈使用体验上的问题?想找求职搭子?
|
||||
|
||||
<img width="360" alt="QQ Group QRCode" src="https://github.com/user-attachments/assets/67560948-467b-44f5-a3e3-c8713255d8b7" />
|
||||
|
||||
|
||||
欢迎进群聊天(刚建的群,人有点少,大胆聊就好)
|
||||
|
||||
--------
|
||||
|
||||
@@ -156,3 +173,13 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
更多功能还在开发中~
|
||||
|
||||
祝你求职成功,事业顺利,事事顺心
|
||||
|
||||
## Star
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date" />
|
||||
</picture>
|
||||
|
||||
感谢支持
|
||||
|
||||
13
package.json
13
package.json
@@ -11,11 +11,14 @@
|
||||
"author": "geekgeekrun",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"dayjs": "^1.11.10",
|
||||
"json5": "^2.2.3",
|
||||
"minimist": "^1.2.8",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer": "24.19.0",
|
||||
"puppeteer-extra": "3.3.6",
|
||||
"puppeteer-extra-plugin-anonymize-ua": "2.4.6",
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tapable": "^2.2.1"
|
||||
@@ -27,6 +30,12 @@
|
||||
"node": "20.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": "=8.15.9"
|
||||
"pnpm": ">=8.15.9 <9.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"find-chrome-bin@2.0.4": "patches/find-chrome-bin@2.0.4.patch",
|
||||
"puppeteer-extra-plugin-anonymize-ua@2.4.6": "patches/puppeteer-extra-plugin-anonymize-ua@2.4.6.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7492
packages/geek-auto-start-chat-with-boss/cityGroup.mjs
Normal file
7492
packages/geek-auto-start-chat-with-boss/cityGroup.mjs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,9 @@ function combine(arr, min, max) {
|
||||
// 生成符合"0"限制的组合
|
||||
function combineWithZero(arr, min, max) {
|
||||
let combineResult;
|
||||
if (!Array.isArray(arr)) {
|
||||
arr = []
|
||||
}
|
||||
if (arr.includes(0)) {
|
||||
// 如果包含 0,0不参与组合
|
||||
combineResult = [].concat(
|
||||
@@ -56,10 +59,11 @@ function combineWithZero(arr, min, max) {
|
||||
}
|
||||
|
||||
export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
const { salaryList, experienceList, degreeList, scaleList, industryList } =
|
||||
const { cityList, salaryList, experienceList, degreeList, scaleList, industryList } =
|
||||
selectedFilters;
|
||||
|
||||
// 生成符合限制条件的组合
|
||||
const cityComb = combineWithZero(cityList, 0, 1) // Salary: 0-1 个
|
||||
const salaryComb = combineWithZero(salaryList, 0, 1) // Salary: 0-1 个
|
||||
const experienceComb = combineWithZero(experienceList, 0, experienceList.length) // Experience: 0 个或更多
|
||||
const degreeComb = combineWithZero(degreeList, 0, degreeList.length) // Degree: 0 个或更多
|
||||
@@ -67,17 +71,20 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
|
||||
|
||||
// 通过迭代生成所有组合
|
||||
for (const salary of salaryComb) {
|
||||
for (const experience of experienceComb) {
|
||||
for (const degree of degreeComb) {
|
||||
for (const scale of scaleComb) {
|
||||
for (const industry of industryComb) {
|
||||
yield {
|
||||
salaryList: salary,
|
||||
experienceList: experience,
|
||||
degreeList: degree,
|
||||
scaleList: scale,
|
||||
industryList: industry
|
||||
for (const city of cityComb) {
|
||||
for (const salary of salaryComb) {
|
||||
for (const experience of experienceComb) {
|
||||
for (const degree of degreeComb) {
|
||||
for (const scale of scaleComb) {
|
||||
for (const industry of industryComb) {
|
||||
yield {
|
||||
cityList: city,
|
||||
salaryList: salary,
|
||||
experienceList: experience,
|
||||
degreeList: degree,
|
||||
scaleList: scale,
|
||||
industryList: industry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,8 +97,9 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
//#region get count of combinations
|
||||
|
||||
// 计算符合限制条件的组合数量
|
||||
export function calculateTotalCombinations(selectedFilters) {
|
||||
export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) {
|
||||
const {
|
||||
cityList = [],
|
||||
salaryList = [],
|
||||
experienceList = [],
|
||||
degreeList = [],
|
||||
@@ -100,14 +108,70 @@ export function calculateTotalCombinations(selectedFilters) {
|
||||
} = selectedFilters
|
||||
|
||||
// 生成符合限制条件的组合
|
||||
const cityComb = combineWithZero(cityList, 0, 1) // City: 0-1 个
|
||||
const salaryComb = combineWithZero(salaryList, 0, 1) // Salary: 0-1 个
|
||||
const experienceComb = combineWithZero(experienceList, 0, experienceList.length) // Experience: 0 个或更多
|
||||
const degreeComb = combineWithZero(degreeList, 0, degreeList.length) // Degree: 0 个或更多
|
||||
const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多
|
||||
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
|
||||
|
||||
return [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
|
||||
let result = [cityComb, salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
|
||||
return accu * cur.length
|
||||
}, 1)
|
||||
if (!includeEmptyCondition) {
|
||||
result -= 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
//#endregion
|
||||
|
||||
export function checkAnyCombineBossRecommendFilterHasCondition(value) {
|
||||
if (!Object.keys(value ?? {}).length) {
|
||||
return false
|
||||
}
|
||||
return Object.keys(value).some((k) => {
|
||||
return !!value[k]?.length
|
||||
})
|
||||
}
|
||||
|
||||
export function getStaticCombineFilterKey(condition) {
|
||||
const kAsO = {}
|
||||
for (const key of Object.keys(condition ?? []).sort()) {
|
||||
if (condition[key] === null || condition[key] === undefined) {
|
||||
continue
|
||||
}
|
||||
kAsO[key] = condition[key]
|
||||
}
|
||||
return JSON.stringify(kAsO)
|
||||
}
|
||||
|
||||
export function formatStaticCombineFilters(rawStaticCombineRecommendJobFilterConditions) {
|
||||
rawStaticCombineRecommendJobFilterConditions = JSON.parse(JSON.stringify(rawStaticCombineRecommendJobFilterConditions))
|
||||
const map = new Map()
|
||||
for (const condition of rawStaticCombineRecommendJobFilterConditions ?? []) {
|
||||
const key = getStaticCombineFilterKey(condition)
|
||||
map.set(key, condition)
|
||||
}
|
||||
const conditions = Array.from(map.values())
|
||||
const result = conditions.map((condition) => {
|
||||
return {
|
||||
cityList: condition.city ? [condition.city] : [],
|
||||
salaryList: condition.salary ? [condition.salary] : [],
|
||||
experienceList: condition.experience ? [condition.experience] : [],
|
||||
degreeList: condition.degree ? [condition.degree] : [],
|
||||
scaleList: condition.scale ? [condition.scale] : [],
|
||||
industryList: condition.industry ? [condition.industry] : []
|
||||
}
|
||||
})
|
||||
if (!result.length) {
|
||||
result.push({
|
||||
cityList: [],
|
||||
salaryList: [],
|
||||
experienceList: [],
|
||||
degreeList: [],
|
||||
scaleList: [],
|
||||
industryList: []
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
7
packages/geek-auto-start-chat-with-boss/constant.mjs
Normal file
7
packages/geek-auto-start-chat-with-boss/constant.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
export const activeDescList = [
|
||||
'', "半年前活跃","近半年活跃","5月内活跃","4月内活跃","3月内活跃","2月内活跃","本月活跃","2周内活跃","本周活跃","3日内活跃","昨日活跃","今日活跃","刚刚活跃"
|
||||
]
|
||||
|
||||
export const RECOMMEND_JOB_ENTRY_SELECTOR = `.c-expect-select a[ka="jobs_recommend_tab_click"]`
|
||||
export const USER_SET_EXPECT_JOB_ENTRIES_SELECTOR = `.c-expect-select .expect-list .expect-item`
|
||||
export const SEARCH_BOX_SELECTOR = `.c-search-input .search-input-box`
|
||||
@@ -1,18 +1,40 @@
|
||||
{
|
||||
"combineRecommendJobFilterType": 1,
|
||||
"anyCombineRecommendJobFilter": {
|
||||
"cityList": [],
|
||||
"salaryList": [],
|
||||
"experienceList": [],
|
||||
"degreeList": [],
|
||||
"scaleList": [],
|
||||
"industryList": []
|
||||
},
|
||||
"staticCombineRecommendJobFilterConditions": [],
|
||||
"isSkipEmptyConditionForCombineRecommendJobFilter": false,
|
||||
"expectJobRegExpStr": "",
|
||||
"jobNotMatchStrategy": 1,
|
||||
"jobNotActiveStrategy": 1,
|
||||
"markAsNotActiveSelectedTimeRange": 7,
|
||||
"expectCityList": [],
|
||||
"expectCityNotMatchStrategy": 3,
|
||||
"strategyScopeOptionWhenMarkJobCityNotMatch": 2,
|
||||
"jobDetailRegExpMatchLogic": 1,
|
||||
"autoReminder": {
|
||||
"throttleIntervalMinutes": 10,
|
||||
"rechatLimitDay": 21,
|
||||
"geminiApiKey": "",
|
||||
"rechatContentSource": 1,
|
||||
"recentMessageQuantityForLlm": 8,
|
||||
"rechatLlmFallback": 1
|
||||
}
|
||||
"rechatLlmFallback": 1,
|
||||
"onlyRemindBossWithExpectJobType": true
|
||||
},
|
||||
"jobSourceList": [
|
||||
{
|
||||
"type": "expect",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"isSageTimeEnabled": true,
|
||||
"sageTimeOpTimes": 100,
|
||||
"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 @@
|
||||
[
|
||||
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
|
||||
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","巨量引擎","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","京东","沃东天骏","达达","达冠","百度","昆仑芯","小度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","智图","长地万方","瑞图万方","道道通","小熊博望","理想","蔚来","顺丰","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","淘宝","淘麦郎","天猫","盒马","口碑","优视","夸克","UC","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","得物","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","首汽租车","GoFun","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手","Grab","爱回收","洋钱罐","瓴岳","得到","思维造物","地平线","咪咕","翼支付","电信","天翼","联通","蓝湖","墨刀"
|
||||
]
|
||||
[]
|
||||
File diff suppressed because it is too large
Load Diff
54
packages/geek-auto-start-chat-with-boss/sage-time.mjs
Normal file
54
packages/geek-auto-start-chat-with-boss/sage-time.mjs
Normal file
@@ -0,0 +1,54 @@
|
||||
import { readConfigFile } from './runtime-file-utils.mjs'
|
||||
import { sleep } from "@geekgeekrun/utils/sleep.mjs"
|
||||
let {
|
||||
isSageTimeEnabled,
|
||||
sageTimeOpTimes,
|
||||
sageTimePauseMinute,
|
||||
} = readConfigFile('boss.json')
|
||||
isSageTimeEnabled = isSageTimeEnabled ?? true
|
||||
sageTimeOpTimes =
|
||||
isNaN(parseInt(sageTimeOpTimes)) ||
|
||||
parseInt(sageTimeOpTimes) < 1
|
||||
? 100
|
||||
: parseInt(sageTimeOpTimes)
|
||||
sageTimePauseMinute =
|
||||
isNaN(parseFloat(sageTimePauseMinute)) ||
|
||||
parseFloat(sageTimePauseMinute) < 0
|
||||
? 15
|
||||
: parseFloat(sageTimePauseMinute)
|
||||
if (parseFloat(sageTimePauseMinute) === 0) {
|
||||
isSageTimeEnabled = false
|
||||
}
|
||||
|
||||
let totalEnabledTimes = 0
|
||||
let recordedOpCount = 0
|
||||
export const waitForSageTimeOrJustContinue = async ({
|
||||
tag,
|
||||
hooks,
|
||||
} = {}) => {
|
||||
if (!isSageTimeEnabled) {
|
||||
return
|
||||
}
|
||||
const tagText = tag ? ` ${tag}` : ''
|
||||
if (recordedOpCount > sageTimeOpTimes) {
|
||||
totalEnabledTimes++
|
||||
console.log(`[SageTime${tagText}] 请求已达限制,开启;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次开启`)
|
||||
await hooks?.sageTimeEnter?.promise({
|
||||
tag,
|
||||
totalEnabledTimes,
|
||||
recordedOpCount,
|
||||
})
|
||||
await sleep(sageTimePauseMinute * 60 * 1000)
|
||||
console.log(`[SageTime${tagText}] 请求限制已解除,关闭;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次关闭`)
|
||||
await hooks?.sageTimeExit?.promise({
|
||||
tag,
|
||||
totalEnabledTimes,
|
||||
recordedOpCount,
|
||||
})
|
||||
recordedOpCount = 0
|
||||
}
|
||||
else {
|
||||
console.log(`[SageTime${tagText}] 请求未达限制;当前记录次数 ${recordedOpCount};已开启过 ${totalEnabledTimes} 次`)
|
||||
recordedOpCount++
|
||||
}
|
||||
}
|
||||
148
packages/laodeng/index.js
Normal file
148
packages/laodeng/index.js
Normal file
@@ -0,0 +1,148 @@
|
||||
"use strict";
|
||||
|
||||
const { PuppeteerExtraPlugin } = require("puppeteer-extra-plugin");
|
||||
async function handle(p) {
|
||||
await p.evaluateOnNewDocument(() => {
|
||||
(() => {
|
||||
"use strict";
|
||||
/* -------------------------------------------------------
|
||||
* 1. 保存原生 Function.prototype.toString
|
||||
* ----------------------------------------------------- */
|
||||
const nativeFunctionToString = Function.prototype.toString;
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* 2. WeakMap:函数 → 伪原生源码
|
||||
* ----------------------------------------------------- */
|
||||
const nativeSourceMap = new WeakMap();
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* 3. 注册伪原生源码
|
||||
* ----------------------------------------------------- */
|
||||
const registerNativeSource = (fn, source) => {
|
||||
try {
|
||||
nativeSourceMap.set(fn, source);
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* 4. 劫持 Function.prototype.toString
|
||||
* ----------------------------------------------------- */
|
||||
Object.defineProperty(Function.prototype, "toString", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function toString() {
|
||||
if (nativeSourceMap.has(this)) {
|
||||
return nativeSourceMap.get(this);
|
||||
}
|
||||
return nativeFunctionToString.call(this);
|
||||
},
|
||||
});
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* 5. 伪装 Function.prototype.toString 自身
|
||||
* ----------------------------------------------------- */
|
||||
registerNativeSource(
|
||||
Function.prototype.toString,
|
||||
nativeFunctionToString.toString(),
|
||||
);
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* 6. stealthify:包装函数但保持“原生外观”
|
||||
* ----------------------------------------------------- */
|
||||
const stealthify = (obj, prop, handler) => {
|
||||
const original = obj[prop];
|
||||
if (typeof original !== "function") return;
|
||||
|
||||
const wrapped = function (...args) {
|
||||
return handler.call(this, original, args);
|
||||
};
|
||||
const namePropertyDescriptor = Object.getOwnPropertyDescriptor(
|
||||
wrapped,
|
||||
"name",
|
||||
);
|
||||
// 处理函数 name 属性
|
||||
Object.defineProperty(wrapped, "name", {
|
||||
...namePropertyDescriptor,
|
||||
value: prop,
|
||||
});
|
||||
// 保留 prototype(某些函数有)
|
||||
try {
|
||||
Object.setPrototypeOf(wrapped, Object.getPrototypeOf(original));
|
||||
} catch (_) {}
|
||||
|
||||
// 注册伪原生源码(直接复用原函数的 native 表现)
|
||||
registerNativeSource(wrapped, nativeFunctionToString.call(original));
|
||||
|
||||
// 用 defineProperty 保持 descriptor 接近原生
|
||||
const desc = Object.getOwnPropertyDescriptor(obj, prop);
|
||||
Object.defineProperty(obj, prop, {
|
||||
...desc,
|
||||
value: wrapped,
|
||||
});
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* 7. 示例:stealth console.log / debug / info
|
||||
* ----------------------------------------------------- */
|
||||
const filterConsoleArgs = (args) =>
|
||||
args.map((arg) => {
|
||||
if (arg && typeof arg === "object") {
|
||||
// 防止 getter / Proxy / 大对象触发
|
||||
return {};
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
|
||||
[
|
||||
"log",
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error",
|
||||
"dir",
|
||||
"table",
|
||||
"debug",
|
||||
].forEach((name) => {
|
||||
stealthify(console, name, (original, args) => {
|
||||
// ❗不传递原始对象,避免 DevTools / CDP 展开
|
||||
return original.apply(console, filterConsoleArgs(args));
|
||||
});
|
||||
});
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* 8. 防御性补丁(可选但强烈建议)
|
||||
* ----------------------------------------------------- */
|
||||
|
||||
// 防止检测 toString 被替换
|
||||
registerNativeSource(
|
||||
registerNativeSource,
|
||||
"function registerNativeSource() { [native code] }",
|
||||
);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
class Plugin extends PuppeteerExtraPlugin {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "laodeng";
|
||||
}
|
||||
|
||||
async onBrowser(browser) {
|
||||
const pages = await browser.pages();
|
||||
for (const p of pages) {
|
||||
await handle(p);
|
||||
}
|
||||
}
|
||||
|
||||
async onPageCreated(p) {
|
||||
await handle(p);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (pluginConfig) {
|
||||
return new Plugin(pluginConfig);
|
||||
};
|
||||
27
packages/laodeng/package.json
Normal file
27
packages/laodeng/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@geekgeekrun/puppeteer-extra-plugin-laodeng",
|
||||
"version": "0.0.1",
|
||||
"description": "laodeng",
|
||||
"main": "./index.js",
|
||||
"author": "geekgeekrun",
|
||||
"productName": "GeekGeekRun",
|
||||
"homepage": "https://github.com/geekgeekrun",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin": "^3.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright-extra": "*",
|
||||
"puppeteer": "^24.19.0",
|
||||
"puppeteer-extra": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"puppeteer-extra": {
|
||||
"optional": true
|
||||
},
|
||||
"playwright-extra": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -17,7 +17,6 @@ import {
|
||||
runtimeFolderPath,
|
||||
ensureEditThisCookie,
|
||||
editThisCookieExtensionPath,
|
||||
isRunFromUi,
|
||||
} from './utils.mjs'
|
||||
|
||||
import { EventEmitter } from 'node:events'
|
||||
@@ -31,9 +30,8 @@ export async function main() {
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: [
|
||||
`--load-extension=${editThisCookieExtensionPath}`
|
||||
]
|
||||
pipe: true,
|
||||
enableExtensions: [editThisCookieExtensionPath]
|
||||
})
|
||||
|
||||
const closeAttachedSet = new WeakSet()
|
||||
@@ -56,10 +54,8 @@ export async function main() {
|
||||
|
||||
page.once('close', async () => {
|
||||
browser.close()
|
||||
if (isRunFromUi) {
|
||||
const electron = await import('electron')
|
||||
electron.app.quit()
|
||||
}
|
||||
const electron = await import('electron')
|
||||
electron.app.quit()
|
||||
})
|
||||
|
||||
const { dispose: disposeNavigationLock } = await blockNavigation(page, (req) => !req.url().startsWith('https://www.zhipin.com'))
|
||||
|
||||
@@ -4,7 +4,6 @@ import os from 'node:os'
|
||||
import extractZip from 'extract-zip'
|
||||
import packageJson from './package.json' assert {type: 'json'}
|
||||
|
||||
export const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
const isUiDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
export const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
@@ -28,34 +27,25 @@ async function getEditThisCookieZipPath () {
|
||||
if (editThisCookieZipPath) {
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
if (isRunFromUi) {
|
||||
const { app } = await import('electron')
|
||||
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
|
||||
} else {
|
||||
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
|
||||
}
|
||||
const { app } = await import('electron')
|
||||
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
|
||||
const APP_GEEKGEEKRUN_EDIT_VERSION = 1
|
||||
export async function ensureEditThisCookie () {
|
||||
let isNeedExtractEditThisCookie = false
|
||||
const manifestFilePath = path.join(editThisCookieExtensionPath, 'manifest.json')
|
||||
if (!fs.existsSync(
|
||||
manifestFilePath
|
||||
)) {
|
||||
const GEEKGEEKRUN_EDIT_VERSION_FILE_PATH = path.join(editThisCookieExtensionPath, 'GEEKGEEKRUN_EDIT_VERSION')
|
||||
let geekgeekrunEditVersion
|
||||
try {
|
||||
const fileContent = fs.readFileSync(GEEKGEEKRUN_EDIT_VERSION_FILE_PATH, { encoding: 'utf-8' })
|
||||
geekgeekrunEditVersion = Number(fileContent) || 0
|
||||
}
|
||||
catch (err) {
|
||||
geekgeekrunEditVersion = 0
|
||||
}
|
||||
if (geekgeekrunEditVersion < APP_GEEKGEEKRUN_EDIT_VERSION) {
|
||||
isNeedExtractEditThisCookie = true
|
||||
} else {
|
||||
let manifest
|
||||
try {
|
||||
manifest = JSON.parse(fs.readFileSync(manifestFilePath, { encoding: 'utf-8' }))
|
||||
if (!manifest.manifest_version || manifest.manifest_version <= 2) {
|
||||
isNeedExtractEditThisCookie = true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.log(`未能获取到文件内容`)
|
||||
isNeedExtractEditThisCookie = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isNeedExtractEditThisCookie) {
|
||||
|
||||
486
packages/pm/daemon.js
Normal file
486
packages/pm/daemon.js
Normal file
@@ -0,0 +1,486 @@
|
||||
// 如果是通过 Electron 运行,禁用 GUI 和 Dock 图标
|
||||
if (typeof require !== 'undefined') {
|
||||
try {
|
||||
const { app } = require('electron');
|
||||
if (app) {
|
||||
// 隐藏 Dock 图标(macOS)
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock?.hide();
|
||||
}
|
||||
|
||||
// 防止显示窗口
|
||||
app.on('ready', () => {
|
||||
// 不创建任何窗口,保持后台运行
|
||||
});
|
||||
|
||||
// 防止在没有窗口时退出
|
||||
app.on('window-all-closed', (e) => {
|
||||
e.preventDefault();
|
||||
// 不退出应用,保持后台运行
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 不在 Electron 环境中,忽略
|
||||
}
|
||||
}
|
||||
|
||||
const net = require('net');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const split2 = require('split2');
|
||||
const fs = require('fs')
|
||||
const { tmpdir } = require('os')
|
||||
const { randomUUID } = require('crypto')
|
||||
|
||||
const ipcWritePipe = fs.createWriteStream(null, { fd: 3 })
|
||||
let ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
|
||||
if (!ipcSocketName) {
|
||||
process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}`
|
||||
ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
|
||||
}
|
||||
const ipcSocketPath = process.platform === 'win32'
|
||||
? `\\\\.\\pipe\\${ipcSocketName}`
|
||||
: path.join(tmpdir(), `${ipcSocketName}.sock`)
|
||||
|
||||
const workers = new Map(); // workerId -> { process, status, restartCount, socket, latestScreenshot, latestScreenshotAt }
|
||||
const userProcessClients = new Set(); // GUI客户端连接集合
|
||||
const stoppedWorkers = new Set(); // 被用户主动停止的workerId集合,用于防止竞态条件
|
||||
const pidToProcessInfoMap = new Map()
|
||||
const socketToWorkerIdSetMap = new WeakMap()
|
||||
|
||||
// 创建TCP服务器
|
||||
const server = net.createServer((socket) => {
|
||||
console.log('客户端已连接');
|
||||
|
||||
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON)
|
||||
// split2 会自动处理 TCP 分包问题,确保每条完整的消息(以换行符结尾)才会触发
|
||||
const splitStream = split2();
|
||||
|
||||
socket.pipe(splitStream).on('data', (line) => {
|
||||
const trimmedLine = line.toString().trim();
|
||||
if (!trimmedLine) {
|
||||
return; // 跳过空行
|
||||
}
|
||||
|
||||
let _callbackUuid
|
||||
try {
|
||||
const message = JSON.parse(trimmedLine);
|
||||
_callbackUuid = message._callbackUuid
|
||||
handleMessage(socket, message);
|
||||
} catch (parseError) {
|
||||
console.error('解析JSON消息失败:', parseError.message);
|
||||
console.error('原始数据:', trimmedLine.substring(0, 100)); // 只打印前100个字符
|
||||
sendResponse(socket, _callbackUuid, { error: '无效的JSON格式', details: parseError.message });
|
||||
}
|
||||
});
|
||||
|
||||
splitStream.on('error', (err) => {
|
||||
console.error('split2 流处理错误:', err);
|
||||
sendResponse(socket, null, { error: '流处理失败' });
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket错误:', err);
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('客户端已断开连接');
|
||||
// 清理GUI客户端连接
|
||||
userProcessClients.delete(socket);
|
||||
// 清理工具进程连接
|
||||
for (const [workerId, workerInfo] of workers.entries()) {
|
||||
if (workerInfo.socket === socket) {
|
||||
console.log(`工具进程 ${workerId} 的连接已断开`);
|
||||
workerInfo.socket = null;
|
||||
}
|
||||
}
|
||||
const workerIdSet = socketToWorkerIdSetMap.get(socket) || new Set()
|
||||
;[...workerIdSet].forEach(workerId => {
|
||||
stopWorker(workerId);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// 处理消息
|
||||
function handleMessage(socket, message) {
|
||||
console.log('收到消息:', message);
|
||||
const _callbackUuid = message._callbackUuid
|
||||
if (message.type === 'ping') {
|
||||
sendResponse(socket, _callbackUuid, {
|
||||
success: true,
|
||||
message: 'pong'
|
||||
});
|
||||
return
|
||||
}
|
||||
if (message.type === 'user-process-register') {
|
||||
// GUI客户端的控制消息
|
||||
// 标记为GUI客户端
|
||||
if (!userProcessClients.has(socket)) {
|
||||
userProcessClients.add(socket);
|
||||
}
|
||||
sendResponse(socket, _callbackUuid, {
|
||||
success: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const workerId = message.workerId;
|
||||
const workerInfo = workers.get(workerId);
|
||||
switch (message.type) {
|
||||
// 从工具进程发送的消息(数据、心跳、截图等)
|
||||
case 'check-should-exit': {
|
||||
const workerId = message.workerId;
|
||||
const shouldExit = stoppedWorkers.has(workerId) || !workers.has(workerId);
|
||||
|
||||
sendResponse(socket, _callbackUuid, {
|
||||
workerId: workerId,
|
||||
shouldExit: shouldExit
|
||||
});
|
||||
|
||||
if (shouldExit) {
|
||||
console.log(`工具进程 ${workerId} 查询是否应该退出,返回: 是`);
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'worker-to-gui-message': {
|
||||
// 转发工具进程消息到GUI客户端
|
||||
broadcastToGUI({
|
||||
type: 'worker-to-gui-message',
|
||||
workerId: workerId,
|
||||
data: message.data || message,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
return
|
||||
}
|
||||
case 'worker-screenshot': {
|
||||
if (workerInfo && message.data && message.data.screenshot /* && workerInfo.socket === socket */) {
|
||||
// 如果携带截图信息,则在守护进程内缓存一份,供 get-status 使用
|
||||
try {
|
||||
workerInfo.latestScreenshot = message.data.screenshot;
|
||||
workerInfo.latestScreenshotAt = Date.now();
|
||||
} catch (e) {
|
||||
console.error('缓存 worker 截图信息失败:', e);
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 从GUI进程发送的消息(数据、心跳、截图等)
|
||||
case 'start-worker': {
|
||||
const {
|
||||
workerId,
|
||||
command,
|
||||
args,
|
||||
env
|
||||
} = message
|
||||
if (workers.has(workerId)) {
|
||||
console.log(`工具进程 ${workerId} 已在运行`);
|
||||
return;
|
||||
}
|
||||
startWorker({
|
||||
workerId,
|
||||
command,
|
||||
args,
|
||||
env
|
||||
});
|
||||
sendResponse(socket, _callbackUuid, {
|
||||
success: true,
|
||||
message: `工具进程 ${message.workerId} 已启动`,
|
||||
workerId: message.workerId
|
||||
});
|
||||
let socketToWorkerIdSet = socketToWorkerIdSetMap.get(socket)
|
||||
if (
|
||||
!(socketToWorkerIdSet instanceof Set)
|
||||
) {
|
||||
socketToWorkerIdSet = new Set()
|
||||
socketToWorkerIdSetMap.set(
|
||||
socket,
|
||||
socketToWorkerIdSet
|
||||
)
|
||||
}
|
||||
socketToWorkerIdSet.add(workerId)
|
||||
return
|
||||
}
|
||||
|
||||
case 'stop-worker': {
|
||||
stopWorker(message.workerId);
|
||||
sendResponse(socket, _callbackUuid, {
|
||||
success: true,
|
||||
message: `工具进程 ${message.workerId} 已停止`,
|
||||
workerId: message.workerId
|
||||
});
|
||||
return
|
||||
}
|
||||
|
||||
case 'get-status': {
|
||||
const status = getWorkersStatus();
|
||||
sendResponse(socket, _callbackUuid, {
|
||||
success: true,
|
||||
workers: status
|
||||
});
|
||||
return
|
||||
}
|
||||
|
||||
default: {
|
||||
sendResponse(socket, _callbackUuid, { error: '未知的消息类型' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 启动工具进程
|
||||
function startWorker({ workerId, command, args, env }, restartCount = 0) {
|
||||
const noAutoRestartExitCodeSet = new Set([0]);
|
||||
(env.GEEKGEEKRUND_NO_AUTO_RESTART_EXIT_CODE ?? '')
|
||||
.split(',')
|
||||
.map(n => parseInt(n))
|
||||
.forEach(n => noAutoRestartExitCodeSet.add(n))
|
||||
|
||||
console.log(`启动工具进程: ${workerId}${restartCount > 0 ? ` (重启第${restartCount}次)` : ''}`);
|
||||
// 添加参数使工具进程在后台运行,不显示 UI
|
||||
const workerProcess = spawn(command, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
GEEKGEEKRUND_WORKER_ID: workerId,
|
||||
GEEKGEEKRUND_RESTART_COUNT: restartCount.toString(),
|
||||
}
|
||||
});
|
||||
|
||||
let output = '';
|
||||
workerProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
console.log(`工具进程 ${workerId} 输出:`, data.toString().trim());
|
||||
});
|
||||
|
||||
workerProcess.stderr.on('data', (data) => {
|
||||
console.error(`工具进程 ${workerId} 错误:`, data.toString().trim());
|
||||
});
|
||||
|
||||
workerProcess.on('exit', (code, signal) => {
|
||||
console.log(`工具进程 ${workerId} 退出,代码: ${code}, 信号: ${signal}`);
|
||||
const workerInfo = pidToProcessInfoMap.get(workerProcess.pid)
|
||||
if (workerInfo) {
|
||||
pidToProcessInfoMap.delete(workerProcess.pid)
|
||||
// 关闭工具进程的TCP连接
|
||||
if (workerInfo.socket) {
|
||||
workerInfo.socket.destroy();
|
||||
}
|
||||
|
||||
const shouldRestart = !noAutoRestartExitCodeSet.has(code) // && code !== null;
|
||||
// 使用当前的 restartCount 加1,而不是从 workerInfo 中取(因为可能已经被删除)
|
||||
const restartCount = (workerInfo.restartCount || 0) + 1;
|
||||
|
||||
workers.delete(workerId);
|
||||
|
||||
// 通知GUI客户端工具进程已退出
|
||||
broadcastToGUI({
|
||||
type: 'worker-exited',
|
||||
workerId: workerId,
|
||||
code: code,
|
||||
signal: signal,
|
||||
restarting: shouldRestart && !stoppedWorkers.has(workerId),
|
||||
restartCount: restartCount
|
||||
});
|
||||
|
||||
// 如果进程意外退出(非正常停止),且不在停止列表中,自动重启
|
||||
if (shouldRestart && !stoppedWorkers.has(workerId)) {
|
||||
console.log(`工具进程 ${workerId} 意外退出,准备重启 (第${restartCount}次)`);
|
||||
|
||||
// 延迟重启,避免频繁重启
|
||||
setTimeout(() => {
|
||||
// 再次检查:确保worker不在停止列表中,且当前没有运行
|
||||
if (!workers.has(workerId) && !stoppedWorkers.has(workerId)) {
|
||||
startWorker({ workerId, command, args, env }, restartCount);
|
||||
} else if (stoppedWorkers.has(workerId)) {
|
||||
console.log(`工具进程 ${workerId} 在重启前被标记为停止,取消重启`);
|
||||
// 从停止列表中移除,因为已经处理完毕
|
||||
stoppedWorkers.delete(workerId);
|
||||
broadcastToGUI({
|
||||
type: 'worker-exited',
|
||||
workerId: workerId,
|
||||
code: code,
|
||||
signal: signal,
|
||||
restarting: false,
|
||||
restartCount: restartCount
|
||||
});
|
||||
}
|
||||
}, 2000);
|
||||
} else if (stoppedWorkers.has(workerId)) {
|
||||
// 如果是在停止列表中,清理标记
|
||||
console.log(`工具进程 ${workerId} 已停止,清理停止标记`);
|
||||
stoppedWorkers.delete(workerId);
|
||||
}
|
||||
} else {
|
||||
// 如果workerInfo不存在,可能是已经被stopWorker删除
|
||||
// 检查停止列表,如果在则清理
|
||||
if (stoppedWorkers.has(workerId)) {
|
||||
stoppedWorkers.delete(workerId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
workerProcess.on('error', (err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
const workerInfo = {
|
||||
process: workerProcess,
|
||||
status: 'running',
|
||||
startTime: Date.now(),
|
||||
restartCount, // 使用传入的重启次数
|
||||
// socket: null, // 工具进程的TCP连接,稍后由工具进程注册
|
||||
// lastHeartbeat: null,
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
workerId,
|
||||
}
|
||||
workers.set(workerId, workerInfo);
|
||||
pidToProcessInfoMap.set(workerProcess.pid, workerInfo);
|
||||
// 定期发送状态更新
|
||||
broadcastStatus();
|
||||
}
|
||||
|
||||
// 停止工具进程
|
||||
function stopWorker(workerId) {
|
||||
const workerInfo = workers.get(workerId);
|
||||
|
||||
// 无论workerInfo是否存在,都添加到停止列表,防止竞态条件
|
||||
stoppedWorkers.add(workerId);
|
||||
console.log(`停止工具进程: ${workerId} (已添加到停止列表)`);
|
||||
if (!workerInfo) {
|
||||
console.log(`工具进程 ${workerId} 不存在,但已标记为停止(防止重启)`);
|
||||
// 通知GUI客户端
|
||||
broadcastToGUI({
|
||||
type: 'worker-disconnected',
|
||||
workerId: workerId,
|
||||
message: `工具进程 ${workerId} 已停止`
|
||||
});
|
||||
// 延迟发送状态更新
|
||||
setTimeout(() => broadcastStatus(), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭工具进程的TCP连接
|
||||
if (workerInfo.socket) {
|
||||
workerInfo.socket.destroy();
|
||||
}
|
||||
|
||||
workerInfo.process.kill('SIGTERM');
|
||||
workers.delete(workerId);
|
||||
|
||||
// 通知GUI客户端
|
||||
broadcastToGUI({
|
||||
type: 'worker-disconnected',
|
||||
workerId: workerId,
|
||||
message: `工具进程 ${workerId} 已断开`
|
||||
});
|
||||
|
||||
// 延迟发送状态更新
|
||||
setTimeout(() => broadcastStatus(), 500);
|
||||
}
|
||||
|
||||
// 获取所有工具进程状态
|
||||
function getWorkersStatus() {
|
||||
const status = [];
|
||||
for (const [workerId, workerInfo] of workers.entries()) {
|
||||
status.push({
|
||||
workerId,
|
||||
status: workerInfo.status,
|
||||
uptime: Date.now() - workerInfo.startTime,
|
||||
restartCount: workerInfo.restartCount || 0,
|
||||
// connected: workerInfo.socket !== null && !workerInfo.socket.destroyed,
|
||||
// lastHeartbeat: workerInfo.lastHeartbeat,
|
||||
command: workerInfo.command,
|
||||
args: workerInfo.args,
|
||||
pid: workerInfo.process?.pid,
|
||||
// 最新截图(通常是 data URL 或 base64 字符串),以及截图时间
|
||||
screenshot: workerInfo.latestScreenshot ?? null,
|
||||
screenshotAt: workerInfo.latestScreenshotAt ?? null,
|
||||
});
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// 广播状态更新给所有GUI客户端
|
||||
function broadcastStatus() {
|
||||
const status = getWorkersStatus();
|
||||
broadcastToGUI({
|
||||
type: 'status',
|
||||
workers: status
|
||||
});
|
||||
}
|
||||
|
||||
// 广播消息给所有GUI客户端
|
||||
function broadcastToGUI(message) {
|
||||
userProcessClients.forEach(socket => {
|
||||
if (!socket.destroyed) {
|
||||
try {
|
||||
sendResponse(socket, null, message);
|
||||
} catch (e) {
|
||||
console.error('广播消息失败:', e);
|
||||
userProcessClients.delete(socket);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
function sendResponse(socket, _callbackUuid, response) {
|
||||
try {
|
||||
socket.write(JSON.stringify({
|
||||
...response,
|
||||
_callbackUuid
|
||||
}) + '\n');
|
||||
} catch (e) {
|
||||
console.error('发送响应失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
new Promise((resolve, reject) => {
|
||||
server.once('error', (err) => {
|
||||
ipcWritePipe.write(
|
||||
JSON.stringify({ type: 'DAEMON_FATAL', error: err }),
|
||||
(err) => void err
|
||||
)
|
||||
reject(err)
|
||||
})
|
||||
server.listen(ipcSocketPath, (err) => {
|
||||
console.log(`守护进程服务器运行在端口 ${ipcSocketPath}`);
|
||||
ipcWritePipe.write(
|
||||
JSON.stringify({ type: 'DAEMON_READY' }),
|
||||
(err) => void err
|
||||
)
|
||||
if (!err) {
|
||||
resolve(true)
|
||||
}
|
||||
else {
|
||||
reject(err)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('收到SIGTERM信号,正在关闭所有工具进程...');
|
||||
for (const [workerId, workerInfo] of workers.entries()) {
|
||||
workerInfo.process.kill('SIGTERM');
|
||||
}
|
||||
server.close(() => {
|
||||
console.log('守护进程已关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('收到SIGINT信号,正在关闭所有工具进程...');
|
||||
for (const [workerId, workerInfo] of workers.entries()) {
|
||||
workerInfo.process.kill('SIGTERM');
|
||||
}
|
||||
server.close(() => {
|
||||
console.log('守护进程已关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
25
packages/pm/package.json
Normal file
25
packages/pm/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@geekgeekrun/pm",
|
||||
"version": "0.0.1",
|
||||
"description": "Electron多进程架构",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start:daemon": "electron ./daemon",
|
||||
"dev:daemon": "electron ./daemon --dev",
|
||||
"start:worker": "electron ./worker",
|
||||
"dev:worker": "electron ./worker --dev"
|
||||
},
|
||||
"keywords": [
|
||||
"electron",
|
||||
"multi-process"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "28.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"JSONStream": "^1.3.5",
|
||||
"split2": "^4.2.0"
|
||||
}
|
||||
}
|
||||
@@ -47,15 +47,20 @@ const main = async () => {
|
||||
}
|
||||
const hooks = {
|
||||
daemonInitialized: new AsyncSeriesHook(),
|
||||
puppeteerLaunched: new SyncHook(),
|
||||
puppeteerLaunched: new SyncHook(['browser']),
|
||||
pageGotten: new SyncHook(['page']),
|
||||
pageLoaded: new SyncHook(),
|
||||
cookieWillSet: new SyncHook(['cookies']),
|
||||
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
||||
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
|
||||
newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']),
|
||||
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
|
||||
noPositionFoundForCurrentJob: new SyncHook(),
|
||||
noPositionFoundAfterTraverseAllJob: new SyncHook(),
|
||||
errorEncounter: new SyncHook(['errorInfo'])
|
||||
errorEncounter: new SyncHook(['errorInfo']),
|
||||
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']),
|
||||
sageTimeEnter: new AsyncSeriesHook(['args']),
|
||||
sageTimeExit: new AsyncSeriesHook(['args'])
|
||||
}
|
||||
initPlugins(hooks)
|
||||
await hooks.daemonInitialized.callAsync()
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "@geekgeekrun/sqlite-plugin",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"dependencies": {
|
||||
"cli-highlight": "^2.1.11",
|
||||
"lodash": "^4.17.21",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"sqlite3": "5.1.6",
|
||||
"typeorm": "0.3.11"
|
||||
"better-sqlite3": "12.6.2",
|
||||
"typeorm": "0.3.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.23",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm();
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = typeorm;
|
||||
|
||||
@Entity()
|
||||
export class AutoStartChatRunRecord {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm();
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = typeorm;
|
||||
|
||||
@Entity()
|
||||
export class BossActiveStatusRecord {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryColumn } = requireTypeorm();
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryColumn } = typeorm;
|
||||
|
||||
@Entity()
|
||||
export class BossInfo {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
|
||||
|
||||
@Entity()
|
||||
export class BossInfoChangeLog {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
|
||||
|
||||
@Entity()
|
||||
export class ChatMessageRecord {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
||||
import { JobSource } from "../enums";
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
|
||||
|
||||
export enum ChatStartupFrom {
|
||||
AutoFromRecommendList = null,
|
||||
@@ -29,4 +30,9 @@ export class ChatStartupLog {
|
||||
nullable: true
|
||||
})
|
||||
autoStartupChatRecordId?: number;
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
jobSource?: JobSource;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryColumn } = requireTypeorm()
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryColumn } = typeorm
|
||||
|
||||
@Entity()
|
||||
export class CompanyInfo {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, PrimaryGeneratedColumn, Column } = requireTypeorm()
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, PrimaryGeneratedColumn, Column } = typeorm
|
||||
|
||||
@Entity()
|
||||
export class CompanyInfoChangeLog {
|
||||
|
||||
15
packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts
Normal file
15
packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { JobHireStatus } from "../enums";
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryColumn } = typeorm;
|
||||
|
||||
@Entity()
|
||||
export class JobHireStatusRecord {
|
||||
@PrimaryColumn()
|
||||
encryptJobId: string;
|
||||
|
||||
@Column()
|
||||
hireStatus: JobHireStatus;
|
||||
|
||||
@Column()
|
||||
lastSeenDate: Date;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryColumn } = requireTypeorm()
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryColumn } = typeorm
|
||||
|
||||
@Entity()
|
||||
export class JobInfo {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, PrimaryGeneratedColumn, Column } = requireTypeorm()
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, PrimaryGeneratedColumn, Column } = typeorm
|
||||
|
||||
@Entity()
|
||||
export class JobInfoChangeLog {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, PrimaryGeneratedColumn, Column, Index } = requireTypeorm()
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, PrimaryGeneratedColumn, Column, Index } = typeorm
|
||||
|
||||
@Entity()
|
||||
@Index(["providerCompleteApiUrl", "model", "providerApiSecret"])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MarkAsNotSuitReason } from "../enums";
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
|
||||
import * as typeorm from 'typeorm';
|
||||
import { ChatStartupFrom } from "./ChatStartupLog";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
|
||||
|
||||
@Entity()
|
||||
export class MarkAsNotSuitLog {
|
||||
@@ -27,6 +27,11 @@ export class MarkAsNotSuitLog {
|
||||
})
|
||||
markReason?: MarkAsNotSuitReason
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
markOp?: MarkAsNotSuitOp
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
@@ -36,4 +41,9 @@ export class MarkAsNotSuitLog {
|
||||
nullable: true
|
||||
})
|
||||
autoStartupChatRecordId?: number;
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
jobSource?: JobSource;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryColumn } = requireTypeorm()
|
||||
import * as typeorm from 'typeorm';
|
||||
const { Entity, Column, PrimaryColumn } = typeorm
|
||||
|
||||
@Entity()
|
||||
export class UserInfo {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
||||
import * as typeorm from 'typeorm';
|
||||
const { ViewEntity, ViewColumn } = typeorm;
|
||||
@ViewEntity({
|
||||
expression: `SELECT
|
||||
boss_info.encryptBossId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
||||
import * as typeorm from 'typeorm';
|
||||
const { ViewEntity, ViewColumn } = typeorm;
|
||||
@ViewEntity({
|
||||
expression: `SELECT
|
||||
job_info.*,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
||||
import * as typeorm from 'typeorm';
|
||||
const { ViewEntity, ViewColumn } = typeorm;
|
||||
@ViewEntity({
|
||||
expression: `SELECT
|
||||
company_info.*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
||||
import * as typeorm from 'typeorm';
|
||||
const { ViewEntity, ViewColumn } = typeorm;
|
||||
@ViewEntity({
|
||||
expression: `SELECT
|
||||
job_info.*,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
import * as typeorm from 'typeorm';
|
||||
import { ChatStartupFrom } from "./ChatStartupLog";
|
||||
import { MarkAsNotSuitReason } from "../enums";
|
||||
const { ViewEntity, ViewColumn } = requireTypeorm();
|
||||
const { ViewEntity, ViewColumn } = typeorm;
|
||||
@ViewEntity({
|
||||
expression: `SELECT
|
||||
job_info.*,
|
||||
|
||||
@@ -3,4 +3,46 @@ export enum MarkAsNotSuitReason {
|
||||
BOSS_INACTIVE = 1,
|
||||
USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON = 2,
|
||||
JOB_NOT_SUIT = 3,
|
||||
JOB_CITY_NOT_SUIT = 4,
|
||||
JOB_WORK_EXP_NOT_SUIT = 5,
|
||||
JOB_SALARY_NOT_SUIT = 6,
|
||||
COMPANY_NAME_NOT_SUIT = 7,
|
||||
}
|
||||
|
||||
export enum MarkAsNotSuitOp {
|
||||
MARK_AS_NOT_SUIT_ON_BOSS = 1,
|
||||
MARK_AS_NOT_SUIT_ON_LOCAL = 2,
|
||||
NO_OP = 3
|
||||
}
|
||||
|
||||
export enum StrategyScopeOptionWhenMarkJobNotMatch {
|
||||
ALL_JOB = 1,
|
||||
ONLY_COMPANY_MATCHED_JOB = 2
|
||||
}
|
||||
|
||||
export enum SalaryCalculateWay {
|
||||
MONTH_SALARY = 1,
|
||||
ANNUAL_PACKAGE = 2,
|
||||
}
|
||||
|
||||
export enum JobDetailRegExpMatchLogic {
|
||||
EVERY = 1,
|
||||
SOME = 2,
|
||||
}
|
||||
|
||||
export enum JobSource {
|
||||
expect = 1,
|
||||
recommend = 2,
|
||||
search = 3,
|
||||
}
|
||||
|
||||
export enum CombineRecommendJobFilterType {
|
||||
ANY_COMBINE = 1,
|
||||
STATIC_COMBINE = 2,
|
||||
}
|
||||
|
||||
export enum JobHireStatus {
|
||||
HIRING = 1,
|
||||
CLOSED = 2,
|
||||
DELETED = 3,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataSource } from "typeorm";
|
||||
import { DataSource, Raw } from "typeorm";
|
||||
import { BossActiveStatusRecord } from "./entity/BossActiveStatusRecord";
|
||||
import { BossInfo } from "./entity/BossInfo";
|
||||
import { CompanyInfo } from "./entity/CompanyInfo";
|
||||
@@ -11,6 +11,7 @@ import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
|
||||
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
|
||||
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
|
||||
import { LlmModelUsageRecord } from "./entity/LlmModelUsageRecord";
|
||||
import { JobHireStatusRecord } from "./entity/JobHireStatusRecord";
|
||||
|
||||
function getBossInfoIfIsEqual (savedOne, currentOne) {
|
||||
if (savedOne === currentOne) {
|
||||
@@ -245,7 +246,7 @@ export async function saveChatStartupRecord(
|
||||
ds: DataSource,
|
||||
_jobInfo,
|
||||
{ encryptUserId },
|
||||
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined } = {}
|
||||
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined, jobSource = undefined } = {}
|
||||
) {
|
||||
const { jobInfo } = _jobInfo;
|
||||
|
||||
@@ -256,7 +257,8 @@ export async function saveChatStartupRecord(
|
||||
encryptCurrentUserId: encryptUserId,
|
||||
encryptJobId: jobInfo.encryptId,
|
||||
autoStartupChatRecordId,
|
||||
chatStartupFrom
|
||||
chatStartupFrom,
|
||||
jobSource,
|
||||
}
|
||||
Object.assign(chatStartupLog, chatStartupLogPayload)
|
||||
|
||||
@@ -270,7 +272,7 @@ export async function saveMarkAsNotSuitRecord(
|
||||
ds: DataSource,
|
||||
_jobInfo,
|
||||
{ encryptUserId },
|
||||
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined } = {}
|
||||
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined, jobSource = undefined } = {}
|
||||
) {
|
||||
const { jobInfo } = _jobInfo;
|
||||
|
||||
@@ -283,7 +285,9 @@ export async function saveMarkAsNotSuitRecord(
|
||||
autoStartupChatRecordId,
|
||||
markFrom,
|
||||
markReason,
|
||||
extInfo: extInfo ? JSON.stringify(extInfo) : undefined
|
||||
extInfo: extInfo ? JSON.stringify(extInfo) : undefined,
|
||||
markOp,
|
||||
jobSource,
|
||||
}
|
||||
Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload)
|
||||
|
||||
@@ -326,3 +330,61 @@ export async function saveGptCompletionRequestRecord(
|
||||
//#endregion
|
||||
return
|
||||
}
|
||||
|
||||
export async function getNotSuitMarkRecordsInLastSomeDays (ds: DataSource, days = 0) {
|
||||
const repo = ds.getRepository(MarkAsNotSuitLog)
|
||||
const result = await repo.findBy({
|
||||
date: Raw(alias => `DATE(${alias}) >= DATE('${
|
||||
new Date(
|
||||
Number(new Date()) - days * 24 * 60 * 60 * 1000
|
||||
).toISOString()
|
||||
}')`)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getChatStartupRecordsInLastSomeDays (ds: DataSource, days = 0) {
|
||||
const repo = ds.getRepository(ChatStartupLog)
|
||||
const result = await repo.findBy({
|
||||
date: Raw(alias => `DATE(${alias}) >= DATE('${
|
||||
new Date(
|
||||
Number(new Date()) - days * 24 * 60 * 60 * 1000
|
||||
).toISOString()
|
||||
}')`)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getBossIdsByJobIds (ds: DataSource, jobIds: string[] = []) {
|
||||
const repo = ds.getRepository(JobInfo)
|
||||
const result = await repo.find({
|
||||
where: jobIds.map(
|
||||
id => ({
|
||||
encryptJobId: id
|
||||
})
|
||||
)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export async function saveJobHireStatusRecord(
|
||||
ds: DataSource,
|
||||
record: JobHireStatusRecord
|
||||
) {
|
||||
const jobHireStatusRecordRepository = ds.getRepository(JobHireStatusRecord);
|
||||
await jobHireStatusRecordRepository.save(record);
|
||||
return
|
||||
}
|
||||
|
||||
export async function getJobHireStatusRecord(
|
||||
ds: DataSource,
|
||||
encryptJobId: string
|
||||
) {
|
||||
const repo = ds.getRepository(JobHireStatusRecord)
|
||||
const result = await repo.findOne({
|
||||
where: {
|
||||
encryptJobId
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import "reflect-metadata";
|
||||
import { type DataSource } from "typeorm";
|
||||
import { requireTypeorm } from "./utils/module-loader";
|
||||
|
||||
import { BossInfo } from "./entity/BossInfo";
|
||||
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
|
||||
@@ -20,22 +19,36 @@ import { VCompanyLibrary } from "./entity/VCompanyLibrary"
|
||||
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
|
||||
import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
||||
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
||||
import { JobHireStatusRecord } from './entity/JobHireStatusRecord'
|
||||
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord } from "./handlers";
|
||||
import {
|
||||
saveChatStartupRecord,
|
||||
saveJobInfoFromRecommendPage,
|
||||
saveMarkAsNotSuitRecord,
|
||||
getNotSuitMarkRecordsInLastSomeDays,
|
||||
getChatStartupRecordsInLastSomeDays,
|
||||
getBossIdsByJobIds,
|
||||
saveJobHireStatusRecord
|
||||
} from "./handlers";
|
||||
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
|
||||
import minimist from 'minimist'
|
||||
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
|
||||
import { JobHireStatus, MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
|
||||
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
|
||||
import { Init1000000000000 } from "./migrations/1000000000000-Init";
|
||||
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
|
||||
import { AddJobHireStatusTable1766466476822 } from "./migrations/1766466476822-AddJobHireStatusTable";
|
||||
import chunk from 'lodash/chunk'
|
||||
import * as typeorm from 'typeorm'
|
||||
|
||||
export function initDb(dbFilePath) {
|
||||
const { DataSource } = requireTypeorm()
|
||||
const { DataSource } = typeorm
|
||||
const appDataSource = new DataSource({
|
||||
type: "sqlite",
|
||||
synchronize: true,
|
||||
type: "better-sqlite3",
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: "simple-console",
|
||||
database: dbFilePath,
|
||||
driver: sqlite3, // The important line
|
||||
entities: [
|
||||
ChatStartupLog,
|
||||
BossInfo,
|
||||
@@ -55,10 +68,15 @@ export function initDb(dbFilePath) {
|
||||
VMarkAsNotSuitLog,
|
||||
ChatMessageRecord,
|
||||
LlmModelUsageRecord,
|
||||
JobHireStatusRecord,
|
||||
],
|
||||
migrations: [
|
||||
Init1000000000000,
|
||||
UpdateChatStartupLogTable1729182577167,
|
||||
UpdateBossInfoTable1732032381304,
|
||||
AddColumnForMarkAsNotSuitLog1746092370665,
|
||||
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526,
|
||||
AddJobHireStatusTable1766466476822
|
||||
],
|
||||
migrationsRun: true
|
||||
});
|
||||
@@ -77,10 +95,29 @@ export default class SqlitePlugin {
|
||||
userInfo = null
|
||||
|
||||
apply(hooks) {
|
||||
hooks.pageGotten.tap(
|
||||
'SqlitePlugin',
|
||||
(page) => {
|
||||
page.on('response', async (response) => {
|
||||
const ds = await this.initPromise;
|
||||
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
|
||||
const data = await response.json()
|
||||
if (data.code === 0) {
|
||||
await saveJobInfoFromRecommendPage(await ds, data.zpData)
|
||||
await saveJobHireStatusRecord(await ds, {
|
||||
encryptJobId: data.zpData.jobInfo.encryptId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
hooks.userInfoResponse.tapPromise(
|
||||
"SqlitePlugin",
|
||||
async (userInfoResponse) => {
|
||||
if (userInfoResponse.code !== 0) {
|
||||
if (!userInfoResponse || userInfoResponse.code !== 0) {
|
||||
return;
|
||||
}
|
||||
const { zpData: userInfo } = userInfoResponse;
|
||||
@@ -97,27 +134,117 @@ export default class SqlitePlugin {
|
||||
return await userInfoRepository.save(user);
|
||||
}
|
||||
);
|
||||
hooks.mainFlowWillLaunch.tapPromise(
|
||||
"SqlitePlugin",
|
||||
async ({
|
||||
jobNotMatchStrategy,
|
||||
jobNotActiveStrategy,
|
||||
expectCityNotMatchStrategy,
|
||||
blockJobNotSuit,
|
||||
blockBossNotActive,
|
||||
blockBossNotNewChat
|
||||
}) => {
|
||||
if (
|
||||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
) {
|
||||
const ds = await this.initPromise;
|
||||
const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7)) ?? [];
|
||||
if (
|
||||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
) {
|
||||
last7DayMarkRecords
|
||||
.filter(it =>
|
||||
[
|
||||
MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||||
MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON
|
||||
].includes(it.markReason)
|
||||
)
|
||||
.map(
|
||||
it => it.encryptJobId
|
||||
)
|
||||
.forEach(
|
||||
id => blockJobNotSuit.add(id)
|
||||
)
|
||||
}
|
||||
if (
|
||||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
) {
|
||||
last7DayMarkRecords
|
||||
.filter(it => it.markReason === MarkAsNotSuitReason.BOSS_INACTIVE)
|
||||
.map(
|
||||
it => it.encryptJobId
|
||||
)
|
||||
.forEach(
|
||||
id => blockJobNotSuit.add(id)
|
||||
)
|
||||
}
|
||||
if (
|
||||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
) {
|
||||
last7DayMarkRecords
|
||||
.filter(it => it.markReason === MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
|
||||
.map(
|
||||
it => it.encryptJobId
|
||||
)
|
||||
.forEach(
|
||||
id => blockJobNotSuit.add(id)
|
||||
)
|
||||
}
|
||||
const last30DayChatStartupRecords = (await getChatStartupRecordsInLastSomeDays(ds, 30)) ?? [];
|
||||
const chattedJobIds = last30DayChatStartupRecords.map(it => it.encryptJobId)
|
||||
if (chattedJobIds.length === 0) {
|
||||
return
|
||||
}
|
||||
const chattedJobIdChunks = chunk(chattedJobIds, 200)
|
||||
const chattedBossIds = [];
|
||||
for (const chattedJobIdChunk of chattedJobIdChunks) {
|
||||
const chattedBossIdChunk = ((await getBossIdsByJobIds(ds, chattedJobIdChunk)) ?? []).map(it => it.encryptBossId)
|
||||
chattedBossIds.push(...chattedBossIdChunk)
|
||||
}
|
||||
for (const id of chattedBossIds) {
|
||||
blockBossNotNewChat.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async (_jobInfo) => {
|
||||
const ds = await this.initPromise;
|
||||
await saveJobInfoFromRecommendPage(ds, _jobInfo);
|
||||
});
|
||||
|
||||
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList } = {}) => {
|
||||
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async ({ jobInfo }) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
|
||||
autoStartupChatRecordId: this.runRecordId,
|
||||
chatStartupFrom
|
||||
return await saveJobHireStatusRecord(ds, {
|
||||
encryptJobId: jobInfo.encryptId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined } = {}) => {
|
||||
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList, jobSource = undefined } = {}) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
|
||||
autoStartupChatRecordId: this.runRecordId,
|
||||
chatStartupFrom,
|
||||
jobSource
|
||||
});
|
||||
});
|
||||
|
||||
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined, jobSource = undefined } = {}) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, {
|
||||
autoStartupChatRecordId: this.runRecordId,
|
||||
markFrom,
|
||||
markReason,
|
||||
extInfo
|
||||
extInfo,
|
||||
markOp,
|
||||
jobSource
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
24
packages/sqlite-plugin/src/migrations/1000000000000-Init.ts
Normal file
24
packages/sqlite-plugin/src/migrations/1000000000000-Init.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
export class Init1000000000000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const tableSchemaStatements = `CREATE TABLE IF NOT EXISTS "boss_info" ("encryptBossId" varchar PRIMARY KEY NOT NULL, "encryptCompanyId" varchar, "name" varchar NOT NULL, "date" datetime NOT NULL, "title" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "boss_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptBossId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "chat_startup_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "encryptCurrentUserId" varchar NOT NULL, "date" datetime NOT NULL, "chatStartupFrom" integer, "autoStartupChatRecordId" integer);
|
||||
CREATE TABLE IF NOT EXISTS "company_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptCompanyId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "company_info" ("encryptCompanyId" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "brandName" varchar NOT NULL, "scaleLow" integer, "scaleHigh" integer, "stageName" varchar, "industryName" varchar);
|
||||
CREATE TABLE IF NOT EXISTS "job_info" ("encryptJobId" varchar PRIMARY KEY NOT NULL, "jobName" varchar NOT NULL, "positionName" varchar NOT NULL, "salaryLow" integer, "salaryHigh" integer, "salaryMonth" integer, "experienceName" varchar NOT NULL, "publishDate" datetime, "degreeName" varchar, "address" varchar, "description" varchar NOT NULL, "encryptBossId" varchar NOT NULL, "encryptCompanyId" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "job_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "boss_active_status_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptBossId" varchar NOT NULL, "lastActiveStatus" varchar, "updateTime" datetime NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "user_info" ("encryptUserId" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "auto_start_chat_run_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "date" datetime NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "mark_as_not_suit_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "encryptCurrentUserId" varchar NOT NULL, "date" datetime NOT NULL, "markFrom" integer, "markReason" integer, "extInfo" varchar, "autoStartupChatRecordId" integer);
|
||||
CREATE TABLE IF NOT EXISTS "chat_message_record" ("mid" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptFromUserId" varchar NOT NULL, "encryptToUserId" varchar NOT NULL, "time" datetime, "type" varchar, "style" varchar, "text" varchar, "imageUrl" varchar, "imageWidth" integer, "imageHeight" integer);
|
||||
CREATE TABLE IF NOT EXISTS "llm_model_usage_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "providerCompleteApiUrl" varchar NOT NULL, "model" varchar NOT NULL, "providerApiSecret" varchar, "completionTokens" integer, "promptTokens" integer, "promptCacheHitTokens" integer, "promptCacheMissTokens" integer, "totalTokens" integer, "requestStartTime" datetime NOT NULL, "requestEndTime" datetime, "hasError" boolean NOT NULL, "errorMessage" varchar NOT NULL, "requestScene" integer);`.split('\n')
|
||||
for(const statement of tableSchemaStatements) {
|
||||
await queryRunner.query(statement);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { VBossLibrary } from "../entity/VBossLibrary";
|
||||
import { VChatStartupLog } from "../entity/VChatStartupLog";
|
||||
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
|
||||
import { VJobLibrary } from "../entity/VJobLibrary";
|
||||
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
|
||||
|
||||
const viewNames = [
|
||||
"v_boss_library",
|
||||
"v_chat_startup_log",
|
||||
"v_company_library",
|
||||
"v_job_library",
|
||||
"v_mark_as_not_suit_log"
|
||||
];
|
||||
const ViewEntities = [
|
||||
VBossLibrary,
|
||||
VChatStartupLog,
|
||||
VCompanyLibrary,
|
||||
VJobLibrary,
|
||||
VMarkAsNotSuitLog,
|
||||
]
|
||||
|
||||
export class UpdateChatStartupLogTable1729182577167
|
||||
implements MigrationInterface
|
||||
{
|
||||
export class UpdateChatStartupLogTable1729182577167 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const viewName of viewNames) {
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
|
||||
}
|
||||
if (await queryRunner.hasTable("boss_active_status_record")) {
|
||||
if (await queryRunner.hasColumn("boss_active_status_record", "updateDate")) {
|
||||
@@ -40,14 +45,17 @@ export class UpdateChatStartupLogTable1729182577167
|
||||
})
|
||||
)
|
||||
}
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
let expression = viewMetadata.expression;
|
||||
if (typeof expression === 'function') {
|
||||
expression = expression(dataSource).getQuery();
|
||||
}
|
||||
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const viewName of viewNames) {
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
||||
}
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE boss_active_status_record RENAME COLUMN updateTime TO updateDate`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { VBossLibrary } from "../entity/VBossLibrary";
|
||||
import { VChatStartupLog } from "../entity/VChatStartupLog";
|
||||
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
|
||||
import { VJobLibrary } from "../entity/VJobLibrary";
|
||||
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
|
||||
|
||||
const viewNames = [
|
||||
"v_boss_library",
|
||||
"v_chat_startup_log",
|
||||
"v_company_library",
|
||||
"v_job_library",
|
||||
"v_mark_as_not_suit_log",
|
||||
];
|
||||
const ViewEntities = [
|
||||
VBossLibrary,
|
||||
VChatStartupLog,
|
||||
VCompanyLibrary,
|
||||
VJobLibrary,
|
||||
VMarkAsNotSuitLog,
|
||||
]
|
||||
|
||||
export class UpdateBossInfoTable1732032381304 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const viewName of viewNames) {
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
|
||||
}
|
||||
if (await queryRunner.hasTable("boss_info")) {
|
||||
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) {
|
||||
@@ -26,6 +33,15 @@ export class UpdateBossInfoTable1732032381304 implements MigrationInterface {
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
let expression = viewMetadata.expression;
|
||||
if (typeof expression === 'function') {
|
||||
expression = expression(dataSource).getQuery();
|
||||
}
|
||||
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { VBossLibrary } from "../entity/VBossLibrary";
|
||||
import { VChatStartupLog } from "../entity/VChatStartupLog";
|
||||
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
|
||||
import { VJobLibrary } from "../entity/VJobLibrary";
|
||||
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
|
||||
|
||||
const ViewEntities = [
|
||||
VBossLibrary,
|
||||
VChatStartupLog,
|
||||
VCompanyLibrary,
|
||||
VJobLibrary,
|
||||
VMarkAsNotSuitLog,
|
||||
]
|
||||
|
||||
export class AddColumnForMarkAsNotSuitLog1746092370665 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
|
||||
}
|
||||
if (await queryRunner.hasTable("mark_as_not_suit_log")) {
|
||||
if (!await queryRunner.hasColumn("mark_as_not_suit_log", "markOp")) {
|
||||
await queryRunner.addColumn(
|
||||
"mark_as_not_suit_log",
|
||||
new TableColumn({
|
||||
name: "markOp",
|
||||
type: "number",
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
let expression = viewMetadata.expression;
|
||||
if (typeof expression === 'function') {
|
||||
expression = expression(dataSource).getQuery();
|
||||
}
|
||||
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm"
|
||||
import { VBossLibrary } from "../entity/VBossLibrary";
|
||||
import { VChatStartupLog } from "../entity/VChatStartupLog";
|
||||
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
|
||||
import { VJobLibrary } from "../entity/VJobLibrary";
|
||||
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
|
||||
import { JobSource } from "../enums";
|
||||
|
||||
const ViewEntities = [
|
||||
VBossLibrary,
|
||||
VChatStartupLog,
|
||||
VCompanyLibrary,
|
||||
VJobLibrary,
|
||||
VMarkAsNotSuitLog,
|
||||
]
|
||||
export class AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
|
||||
}
|
||||
if (await queryRunner.hasTable("mark_as_not_suit_log")) {
|
||||
if (!await queryRunner.hasColumn("mark_as_not_suit_log", "jobSource")) {
|
||||
await queryRunner.addColumn(
|
||||
"mark_as_not_suit_log",
|
||||
new TableColumn({
|
||||
name: "jobSource",
|
||||
type: "number",
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
await queryRunner.query(`UPDATE mark_as_not_suit_log SET jobSource=?`, [JobSource.expect]);
|
||||
}
|
||||
}
|
||||
if (await queryRunner.hasTable("chat_startup_log")) {
|
||||
if (!await queryRunner.hasColumn("chat_startup_log", "jobSource")) {
|
||||
await queryRunner.addColumn(
|
||||
"chat_startup_log",
|
||||
new TableColumn({
|
||||
name: "jobSource",
|
||||
type: "number",
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
await queryRunner.query(`UPDATE chat_startup_log SET jobSource=?`, [JobSource.expect]);
|
||||
}
|
||||
}
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
let expression = viewMetadata.expression;
|
||||
if (typeof expression === 'function') {
|
||||
expression = expression(dataSource).getQuery();
|
||||
}
|
||||
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
export class AddJobHireStatusTable1766466476822 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE IF NOT EXISTS "job_hire_status_record" ("encryptJobId" varchar PRIMARY KEY NOT NULL, "hireStatus" integer NOT NULL, "lastSeenDate" datetime NOT NULL);`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as path from 'node:path';
|
||||
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
const isUiDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
export function requireTypeorm () {
|
||||
const importResult = require('typeorm')
|
||||
return importResult
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
import { presetUno, presetAttributify, presetIcons } from 'unocss'
|
||||
import transformerDirective from "@unocss/transformer-directives";
|
||||
import transformerDirective from '@unocss/transformer-directives'
|
||||
import Replace from 'unplugin-replace/vite'
|
||||
|
||||
process.env = { ...process.env, ...loadEnv(process.env.NODE_ENV!, process.cwd()) }
|
||||
@@ -13,11 +13,15 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
external: []
|
||||
},
|
||||
minify: 'terser'
|
||||
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
|
||||
},
|
||||
plugins: [
|
||||
externalizeDepsPlugin({
|
||||
exclude: ['@geekgeekrun/geek-auto-start-chat-with-boss', '@geekgeekrun/dingtalk-plugin', '@geekgeekrun/utils', 'find-chrome-bin', '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension']
|
||||
exclude: [
|
||||
'@geekgeekrun/utils',
|
||||
'find-chrome-bin',
|
||||
'@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
|
||||
]
|
||||
}),
|
||||
Replace({
|
||||
delimiters: ['', ''],
|
||||
@@ -26,11 +30,11 @@ export default defineConfig({
|
||||
values: [
|
||||
{
|
||||
find: /<measurement_id>/g,
|
||||
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string,
|
||||
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string
|
||||
},
|
||||
{
|
||||
find: /<api_secret>/g,
|
||||
replacement: process.env.VITE_APP_GTAG_API_SECRET as string,
|
||||
replacement: process.env.VITE_APP_GTAG_API_SECRET as string
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -39,7 +43,7 @@ export default defineConfig({
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
minify: 'terser'
|
||||
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
|
||||
}
|
||||
},
|
||||
renderer: {
|
||||
@@ -52,11 +56,11 @@ export default defineConfig({
|
||||
vue(),
|
||||
UnoCSS({
|
||||
presets: [presetUno(), presetAttributify(), presetIcons()],
|
||||
transformers: [transformerDirective()],
|
||||
transformers: [transformerDirective()]
|
||||
})
|
||||
],
|
||||
build: {
|
||||
minify: 'terser'
|
||||
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "geekgeekrun-ui",
|
||||
"version": "0.4.0",
|
||||
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
|
||||
"version": "0.14.2",
|
||||
"description": "BOSS 炸弹 - 自动开聊BOSS,助力每位打工人求职!",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "geekgeekrun",
|
||||
"productName": "GeekGeekRun",
|
||||
@@ -9,10 +9,6 @@
|
||||
"scripts": {
|
||||
"start": "node scripts/run-build-sqlite-plugin.mjs && electron-vite preview",
|
||||
"dev": "node scripts/run-build-sqlite-plugin.mjs && electron-vite dev",
|
||||
"dev:geek-auto-start-chat-with-boss-main-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=geekAutoStartWithBossMain pnpm run dev",
|
||||
"dev:geek-auto-start-chat-with-boss-daemon-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=geekAutoStartWithBossDaemon pnpm run dev",
|
||||
"dev:check-and-download-dependencies-for-init-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=checkAndDownloadDependenciesForInit pnpm run dev",
|
||||
"dev:launch-bosszhipin-login-page-with-preload-extension-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=launchBossZhipinLoginPageWithPreloadExtension pnpm run dev",
|
||||
"build": "node scripts/run-build-sqlite-plugin.mjs && electron-vite build",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
@@ -30,29 +26,35 @@
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@geekgeekrun/dingtalk-plugin": "workspace:*",
|
||||
"@geekgeekrun/geek-auto-start-chat-with-boss": "workspace:*",
|
||||
"@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension": "workspace:*",
|
||||
"@geekgeekrun/pm": "workspace:*",
|
||||
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
|
||||
"@geekgeekrun/sqlite-plugin": "workspace:*",
|
||||
"@geekgeekrun/utils": "workspace:*",
|
||||
"JSONStream": "^1.3.5",
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.8",
|
||||
"mitt": "^3.0.1",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"pinia": "^3.0.2",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer": "24.19.0",
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^11.1.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@geekgeekrun/dingtalk-plugin": "workspace:*",
|
||||
"@geekgeekrun/geek-auto-start-chat-with-boss": "workspace:*",
|
||||
"@inquirer/prompts": "^7.0.1",
|
||||
"@inquirer/select": "^4.0.1",
|
||||
"@puppeteer/browsers": "^2.0.0",
|
||||
"@puppeteer/browsers": "^2.11.0",
|
||||
"@rushstack/eslint-patch": "^1.7.1",
|
||||
"@types/lodash": "^4.17.23",
|
||||
"@types/node": "^18.19.9",
|
||||
"@unocss/preset-attributify": "^0.58.5",
|
||||
"@unocss/preset-icons": "^0.58.5",
|
||||
@@ -62,15 +64,14 @@
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "28.2.0",
|
||||
"electron": "39.2.7",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"element-plus": "^2.5.5",
|
||||
"element-plus": "^2.8.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"find-chrome-bin": "^2.0.1",
|
||||
"find-chrome-bin": "2.0.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"normalize.css": "^8.0.1",
|
||||
"prettier": "^3.2.4",
|
||||
"sass": "^1.70.0",
|
||||
@@ -86,4 +87,4 @@
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 109 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "0.4.0",
|
||||
"buildVersion": 9,
|
||||
"buildTime": 1745903517984,
|
||||
"buildHash": "25e52530228e78c3cf78caa053c7da6bb138e5e2",
|
||||
"version": "0.14.2",
|
||||
"buildVersion": 32,
|
||||
"buildTime": 1770299394728,
|
||||
"buildHash": "1d5fdb9d54c569cb53035dfef0f6157c899ccb06",
|
||||
"name": "geekgeekrun-ui"
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
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,
|
||||
ACCESS_IS_DENIED = 84,
|
||||
PUPPETEER_IS_NOT_EXECUTABLE = 85,
|
||||
AUTO_START_CHAT_DAEMON_PROCESS_SUICIDE = 86,
|
||||
AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87,
|
||||
LLM_UNAVAILABLE = 86,
|
||||
}
|
||||
|
||||
export enum RECHAT_CONTENT_SOURCE {
|
||||
@@ -18,3 +17,9 @@ export enum RECHAT_LLM_FALLBACK {
|
||||
SEND_LOOK_FORWARD_EMOTION = 1,
|
||||
EXIT_REMINDER_PROGRAM = 2
|
||||
}
|
||||
|
||||
export enum RUNNING_STATUS_ENUM {
|
||||
RUNNING = 0,
|
||||
NORMAL_EXITED = 1,
|
||||
ERROR_EXITED = 2
|
||||
}
|
||||
|
||||
18
packages/ui/src/common/prerequisite-step-by-step-check.ts
Normal file
18
packages/ui/src/common/prerequisite-step-by-step-check.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const getAutoStartChatSteps = () => [
|
||||
{
|
||||
id: 'worker-launch',
|
||||
describe: '启动子进程'
|
||||
},
|
||||
{
|
||||
id: 'puppeteer-executable-check',
|
||||
describe: 'Puppeteer 可执行程序检查'
|
||||
},
|
||||
{
|
||||
id: 'basic-cookie-check',
|
||||
describe: 'Cookie 格式检查'
|
||||
},
|
||||
{
|
||||
id: 'login-status-check',
|
||||
describe: '登录状态检查'
|
||||
}
|
||||
]
|
||||
4
packages/ui/src/main/constant.ts
Normal file
4
packages/ui/src/main/constant.ts
Normal file
@@ -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 buildInfo from '../../common/build-info.json'
|
||||
import { ensureStorageFileExist } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import {
|
||||
createFirstLaunchNoticeWindow,
|
||||
firstLaunchNoticeWindow
|
||||
} from '../window/firstLaunchNoticeWindow'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
export const firstLaunchNoticeApproveFlagPath = path.join(
|
||||
os.homedir(),
|
||||
@@ -10,8 +15,28 @@ export const firstLaunchNoticeApproveFlagPath = path.join(
|
||||
'ui-first-launch-notice-flag'
|
||||
)
|
||||
|
||||
export const isFirstLaunchNoticeApproveFlagExist = () => fs.existsSync(firstLaunchNoticeApproveFlagPath)
|
||||
export const isFirstLaunchNoticeApproveFlagExist = () =>
|
||||
fs.existsSync(firstLaunchNoticeApproveFlagPath)
|
||||
export const createFirstLaunchNoticeApproveFlag = () => {
|
||||
ensureStorageFileExist()
|
||||
fs.writeFileSync(firstLaunchNoticeApproveFlagPath, buildInfo.version)
|
||||
}
|
||||
export async function waitForUserApproveAgreement({ windowOption } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
createFirstLaunchNoticeWindow({ ...windowOption })
|
||||
let processDone = false
|
||||
function handler() {
|
||||
processDone = true
|
||||
firstLaunchNoticeWindow.close()
|
||||
}
|
||||
ipcMain.once('first-launch-notice-approve', handler)
|
||||
firstLaunchNoticeWindow.once('closed', () => {
|
||||
ipcMain.off('first-launch-notice-approve', handler)
|
||||
if (processDone) {
|
||||
resolve(true)
|
||||
} else {
|
||||
reject(new Error('USER_CANCELLED'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
23
packages/ui/src/main/features/login-with-cookie-assistant.ts
Normal file
23
packages/ui/src/main/features/login-with-cookie-assistant.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { createCookieAssistantWindow, cookieAssistantWindow } from '../window/cookieAssistantWindow';
|
||||
|
||||
export async function loginWithCookieAssistant({ windowOption } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
createCookieAssistantWindow({ ...windowOption })
|
||||
|
||||
let processDone = false
|
||||
function handler() {
|
||||
processDone = true
|
||||
cookieAssistantWindow.close()
|
||||
}
|
||||
ipcMain.once('cookie-saved', handler)
|
||||
cookieAssistantWindow.once('closed', () => {
|
||||
ipcMain.off('cookie-saved', handler)
|
||||
if (processDone) {
|
||||
resolve(true)
|
||||
} else {
|
||||
reject(new Error('USER_CANCELLED_LOGIN'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
74
packages/ui/src/main/features/run-common.ts
Normal file
74
packages/ui/src/main/features/run-common.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat'
|
||||
import { daemonEE, sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||
import { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db'
|
||||
import minimist from 'minimist'
|
||||
|
||||
export async function runCommon({ mode }) {
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'user-process-register'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
const taskList = (
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'get-status'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
)?.workers
|
||||
const runningTask = taskList?.find((it) => it.workerId === mode)
|
||||
if (runningTask) {
|
||||
const commandlineArgs = minimist(runningTask.args ?? [])
|
||||
const runRecordId = Number(commandlineArgs['run-record-id'])
|
||||
console.log('任务已在运行中')
|
||||
return {
|
||||
runRecordId,
|
||||
isAlreadyRunning: true
|
||||
}
|
||||
}
|
||||
const currentRunRecord = (await saveAndGetCurrentRunRecord())?.data
|
||||
const subProcessEnv = {
|
||||
...process.env,
|
||||
GEEKGEEKRUND_NO_AUTO_RESTART_EXIT_CODE: [
|
||||
AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE,
|
||||
AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID,
|
||||
AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE
|
||||
].join(',')
|
||||
}
|
||||
const args =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? [process.argv[1], `--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
|
||||
: [`--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'start-worker',
|
||||
workerId: mode,
|
||||
command: process.argv[0],
|
||||
args,
|
||||
env: subProcessEnv
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
daemonEE.on('message', (message) => {
|
||||
if (message.type === 'worker-exited') {
|
||||
if (
|
||||
message.workerId === mode &&
|
||||
!message.restarting &&
|
||||
globalThis.GEEKGEEKRUN_PROCESS_ROLE !== 'ui'
|
||||
) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
return {
|
||||
runRecordId: currentRunRecord?.id
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,9 @@ export enum DOWNLOAD_ERROR_EXIT_CODE {
|
||||
DOWNLOAD_ERROR = 80
|
||||
}
|
||||
|
||||
export const checkAndDownloadDependenciesForInit = async () => {
|
||||
export const downloadDependenciesForInit = async () => {
|
||||
process.on('disconnect', () => app.exit())
|
||||
process.on('uncaughtException', () => app.exit(DOWNLOAD_ERROR_EXIT_CODE.DOWNLOAD_ERROR))
|
||||
app.dock?.hide()
|
||||
let pipe: null | fs.WriteStream = null
|
||||
try {
|
||||
@@ -49,7 +50,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
|
||||
timeoutTimer = setTimeout(() => {
|
||||
// will encounter this when network disconnected when downloading
|
||||
promiseWithResolver.reject(new Error('PROGRESS_NOT_CHANGED_TOO_LONG'))
|
||||
}, 5 * 1000)
|
||||
}, 10 * 1000)
|
||||
} else {
|
||||
clearTimeout(throttleProgressTimer)
|
||||
throttleProgressTimer = null
|
||||
@@ -72,7 +73,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
|
||||
)
|
||||
throttleProgressTimer = setTimeout(() => {
|
||||
throttleProgressTimer = null
|
||||
}, 2500)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -93,11 +94,13 @@ export const checkAndDownloadDependenciesForInit = async () => {
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
type: 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR',
|
||||
...err instanceof Error ? {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
} : null
|
||||
...(err instanceof Error
|
||||
? {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
}
|
||||
: null)
|
||||
}) + '\r\n'
|
||||
)
|
||||
await sleep(1000)
|
||||
@@ -8,6 +8,8 @@ export interface BrowserInfo {
|
||||
executablePath: string
|
||||
}
|
||||
|
||||
const CONFIG_VSERION = 2
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
export const lastUsedBrowserRecordFilePath = path.join(
|
||||
runtimeFolderPath,
|
||||
@@ -28,8 +30,13 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
|
||||
}
|
||||
try {
|
||||
const fileContent = (await fsPromise.readFile(lastUsedBrowserRecordFilePath)).toString()
|
||||
const [path, browser] = fileContent.split('\n').map((it) => it.trim())
|
||||
if (!path || !fs.existsSync(path)) {
|
||||
const [path, browser, configVersion] = fileContent.split('\n').map((it) => it.trim())
|
||||
if (
|
||||
!path ||
|
||||
!fs.existsSync(path) ||
|
||||
!Number(configVersion) ||
|
||||
Number(configVersion) < CONFIG_VSERION
|
||||
) {
|
||||
await removeLastUsedAndAvailableBrowserPath()
|
||||
return null
|
||||
}
|
||||
@@ -50,7 +57,7 @@ export const saveLastUsedAndAvailableBrowserInfo = async (browserInfo: BrowserIn
|
||||
}
|
||||
await fsPromise.writeFile(
|
||||
lastUsedBrowserRecordFilePath,
|
||||
[browserInfo.executablePath, browserInfo.browser].join('\n')
|
||||
[browserInfo.executablePath, browserInfo.browser, CONFIG_VSERION].join('\n')
|
||||
)
|
||||
} catch {
|
||||
console.warn('lastUsedBrowserRecordFile write error')
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as path from 'node:path'
|
||||
import * as os from 'node:os'
|
||||
import * as fs from 'node:fs'
|
||||
import type { InstalledBrowser } from '@puppeteer/browsers'
|
||||
import {
|
||||
@@ -8,18 +6,16 @@ import {
|
||||
getLastUsedAndAvailableBrowser,
|
||||
removeLastUsedAndAvailableBrowserPath
|
||||
} from '../browser-history'
|
||||
import { getExecutableFileVersion } from '@geekgeekrun/utils/windows-only/file.mjs'
|
||||
import gtag from '../../../../utils/gtag'
|
||||
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../../common/constant'
|
||||
import { cacheDir } from '../../../../constant'
|
||||
|
||||
const getPuppeteerManagerModule = async () => {
|
||||
let puppeteerManager
|
||||
puppeteerManager = await import('@puppeteer/browsers')
|
||||
const puppeteerManager = await import('@puppeteer/browsers')
|
||||
|
||||
return puppeteerManager
|
||||
}
|
||||
|
||||
const EXPECT_CHROMIUM_BUILD_ID = '113.0.5672.63'
|
||||
const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')
|
||||
|
||||
const getExpectCachedPuppeteerExecutable = async (): Promise<BrowserInfo> => {
|
||||
const puppeteerManager = await getPuppeteerManagerModule()
|
||||
@@ -76,7 +72,8 @@ export const checkAndDownloadPuppeteerExecutable = async (
|
||||
browser: puppeteerManager.Browser.CHROME,
|
||||
cacheDir,
|
||||
buildId: EXPECT_CHROMIUM_BUILD_ID,
|
||||
downloadProgressCallback: options.downloadProgressCallback
|
||||
downloadProgressCallback: options.downloadProgressCallback,
|
||||
baseUrl: `https://registry.npmmirror.com/-/binary/chrome-for-testing`
|
||||
})
|
||||
} else {
|
||||
gtag('use_installed_browser')
|
||||
@@ -98,29 +95,36 @@ export const checkAndDownloadPuppeteerExecutable = async (
|
||||
return installedBrowser
|
||||
}
|
||||
|
||||
export const getAnyAvailablePuppeteerExecutable = async (): Promise<BrowserInfo | null> => {
|
||||
const lastUsedOne = await getLastUsedAndAvailableBrowser()
|
||||
if (lastUsedOne) {
|
||||
return lastUsedOne
|
||||
export const getAnyAvailablePuppeteerExecutable = async ({
|
||||
ignoreCached = false,
|
||||
noSave = false
|
||||
}: {
|
||||
ignoreCached?: boolean
|
||||
noSave?: boolean
|
||||
} = {}): Promise<BrowserInfo | null> => {
|
||||
if (!ignoreCached) {
|
||||
const lastUsedOne = await getLastUsedAndAvailableBrowser()
|
||||
if (lastUsedOne) {
|
||||
return lastUsedOne
|
||||
}
|
||||
}
|
||||
// find existed browser - the fallback one
|
||||
if (await checkCachedPuppeteerExecutable()) {
|
||||
const cachedOne = await getExpectCachedPuppeteerExecutable()
|
||||
!noSave && (await saveLastUsedAndAvailableBrowserInfo(cachedOne))
|
||||
|
||||
return cachedOne
|
||||
}
|
||||
// find existed browser - the one maybe actively installed by user or ship with os like Edge on windows
|
||||
try {
|
||||
const existedOne = await findAndLocateUserInstalledChromiumExecutableSync()
|
||||
await saveLastUsedAndAvailableBrowserInfo(existedOne)
|
||||
!noSave && (await saveLastUsedAndAvailableBrowserInfo(existedOne))
|
||||
// save its path
|
||||
return existedOne
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.log('no existed browser path found')
|
||||
}
|
||||
// find existed browser - the fallback one
|
||||
if (await checkCachedPuppeteerExecutable()) {
|
||||
const cachedOne = await getExpectCachedPuppeteerExecutable()
|
||||
await saveLastUsedAndAvailableBrowserInfo(cachedOne)
|
||||
|
||||
return cachedOne
|
||||
}
|
||||
|
||||
// if no one available, then return null and remove last used browser
|
||||
await removeLastUsedAndAvailableBrowserPath()
|
||||
return null
|
||||
@@ -128,35 +132,10 @@ export const getAnyAvailablePuppeteerExecutable = async (): Promise<BrowserInfo
|
||||
|
||||
export async function findAndLocateUserInstalledChromiumExecutableSync(): Promise<BrowserInfo> {
|
||||
const exceptChromiumMainVersion = Number(EXPECT_CHROMIUM_BUILD_ID.split('.')[0])
|
||||
// For windows, try to find Edge(chromium)
|
||||
if (os.platform() === 'win32') {
|
||||
// TODO: handle windows
|
||||
const edgeExecutableLocation = path.join(
|
||||
process.env['ProgramFiles(x86)']!,
|
||||
'Microsoft/Edge/Application',
|
||||
'msedge.exe'
|
||||
)
|
||||
if (fs.existsSync(edgeExecutableLocation)) {
|
||||
try {
|
||||
const version = await getExecutableFileVersion(edgeExecutableLocation)
|
||||
const mainVersion = Number(version.split('.')[0])
|
||||
if (mainVersion >= exceptChromiumMainVersion) {
|
||||
return {
|
||||
executablePath: edgeExecutableLocation,
|
||||
browser: `Edge ${version}`
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For other, use findChrome
|
||||
let findChrome: typeof import('find-chrome-bin').findChrome
|
||||
findChrome = (await import('find-chrome-bin')).findChrome
|
||||
const findChrome: typeof import('find-chrome-bin').findChrome = (await import('find-chrome-bin'))
|
||||
.findChrome
|
||||
const targetBrowser = await findChrome({
|
||||
min: exceptChromiumMainVersion
|
||||
min: exceptChromiumMainVersion + 1
|
||||
})
|
||||
if (!targetBrowser?.executablePath) {
|
||||
throw new Error('NO_EXPECT_CHROMIUM_FOUND')
|
||||
@@ -1,133 +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.slice(1), `--run-record-id=${runRecordId}`]
|
||||
: [`--run-record-id=${runRecordId}`],
|
||||
{
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'],
|
||||
env: {
|
||||
...process.env,
|
||||
MAIN_BOSSGEEKGO_UI_RUN_MODE: runMode
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,32 @@
|
||||
import DingtalkPlugin from '@geekgeekrun/dingtalk-plugin/index.mjs'
|
||||
import { app } from 'electron'
|
||||
import { app, dialog } from 'electron'
|
||||
import { SyncHook, AsyncSeriesHook } from 'tapable'
|
||||
import {
|
||||
readConfigFile,
|
||||
getPublicDbFilePath
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
|
||||
import * as fs from 'fs'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
|
||||
// import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
|
||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||
|
||||
import minimist from 'minimist'
|
||||
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
|
||||
import gtag from '../../utils/gtag'
|
||||
import GtagPlugin from '../../utils/gtag/GtagPlugin'
|
||||
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||
// import { PeriodPushCurrentPageScreenshotPlugin } from '../../utils/screenshot'
|
||||
import { checkShouldExit } from '../../utils/worker'
|
||||
import { CookieInvalidHandlePlugin } from '../../features/cookie-invalid-handle-plugin'
|
||||
import initPublicIpc from '../../utils/initPublicIpc'
|
||||
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
|
||||
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
|
||||
const { default: SqlitePlugin } = SqlitePluginModule
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('收到SIGTERM信号,正在退出')
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
const rerunInterval = (() => {
|
||||
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
|
||||
if (isNaN(v)) {
|
||||
@@ -33,14 +42,54 @@ const initPlugins = (hooks) => {
|
||||
new DingtalkPlugin(dingTalkAccessToken).apply(hooks)
|
||||
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
|
||||
new GtagPlugin().apply(hooks)
|
||||
// new PeriodPushCurrentPageScreenshotPlugin().apply(hooks)
|
||||
new CookieInvalidHandlePlugin().apply(hooks)
|
||||
}
|
||||
|
||||
let isParentProcessDisconnect = false
|
||||
process.once('disconnect', () => {
|
||||
isParentProcessDisconnect = true
|
||||
})
|
||||
|
||||
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
|
||||
const runAutoChat = async () => {
|
||||
app.dock?.hide()
|
||||
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
if (!puppeteerExecutable) {
|
||||
try {
|
||||
await configWithBrowserAssistant({ autoFind: true })
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
}
|
||||
if (!puppeteerExecutable) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `未找到可用的浏览器`,
|
||||
detail: `请重新运行本程序,按照提示安装、配置浏览器`
|
||||
})
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'puppeteer-executable-check',
|
||||
status: 'rejected'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
|
||||
return
|
||||
}
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'puppeteer-executable-check',
|
||||
status: 'fulfilled'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
|
||||
const { initPuppeteer, mainLoop, closeBrowserWindow, autoStartChatEventBus } = await import(
|
||||
'@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
|
||||
)
|
||||
@@ -48,77 +97,44 @@ const runAutoChat = async () => {
|
||||
closeBrowserWindow()
|
||||
app.exit()
|
||||
})
|
||||
app.dock?.hide()
|
||||
let pipe: null | fs.WriteStream = null
|
||||
try {
|
||||
pipe = fs.createWriteStream(null, { fd: 3 })
|
||||
} catch {
|
||||
console.warn('pipe is not available')
|
||||
}
|
||||
pipeWriteRegardlessError(
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
type: 'INITIALIZE_PUPPETEER'
|
||||
}) + '\r\n'
|
||||
)
|
||||
try {
|
||||
await initPuppeteer()
|
||||
pipeWriteRegardlessError(
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
|
||||
}) + '\r\n'
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
|
||||
return
|
||||
}
|
||||
|
||||
const isPuppeteerExecutable = !!(await getAnyAvailablePuppeteerExecutable())
|
||||
if (!isPuppeteerExecutable) {
|
||||
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
|
||||
return
|
||||
}
|
||||
await initPuppeteer()
|
||||
|
||||
const hooks = {
|
||||
puppeteerLaunched: new SyncHook(),
|
||||
puppeteerLaunched: new SyncHook(['browser']),
|
||||
pageGotten: new SyncHook(['page']),
|
||||
pageLoaded: new SyncHook(),
|
||||
cookieWillSet: new SyncHook(['cookies']),
|
||||
cookieWillSet: new AsyncSeriesHook(['cookies']),
|
||||
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
||||
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
|
||||
jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']),
|
||||
newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']),
|
||||
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
|
||||
jobMarkedAsNotSuit: new AsyncSeriesHook(['positionInfoDetail', 'markDetail']),
|
||||
noPositionFoundForCurrentJob: new SyncHook(),
|
||||
noPositionFoundAfterTraverseAllJob: new SyncHook(),
|
||||
errorEncounter: new SyncHook(['errorInfo'])
|
||||
errorEncounter: new SyncHook(['errorInfo']),
|
||||
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']),
|
||||
sageTimeEnter: new AsyncSeriesHook(['args']),
|
||||
sageTimeExit: new AsyncSeriesHook(['args'])
|
||||
}
|
||||
initPlugins(hooks)
|
||||
|
||||
gtag('run_auto_chat_with_boss_main_ready')
|
||||
pipeWriteRegardlessError(
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
type: 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED' //geek-auto-start-chat-with-boss-started
|
||||
}) + '\r\n'
|
||||
)
|
||||
|
||||
autoStartChatEventBus.once('LOGIN_STATUS_INVALID', () => {
|
||||
pipeWriteRegardlessError(
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started
|
||||
}) + '\r\n'
|
||||
)
|
||||
})
|
||||
|
||||
while (![isParentProcessDisconnect].includes(true)) {
|
||||
while (true) {
|
||||
try {
|
||||
await mainLoop(hooks)
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('LOGIN_STATUS_INVALID')) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `登录状态无效`,
|
||||
detail: `请重新登录BOSS直聘`
|
||||
})
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
|
||||
break
|
||||
}
|
||||
@@ -130,9 +146,18 @@ const runAutoChat = async () => {
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED)
|
||||
break
|
||||
}
|
||||
if (err.message.includes(`Could not find Chrome`) || err.message.includes(`no executable was found`)) {
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
|
||||
break
|
||||
}
|
||||
}
|
||||
closeBrowserWindow?.()
|
||||
console.error(err)
|
||||
const shouldExit = await checkShouldExit()
|
||||
if (shouldExit) {
|
||||
app.exit()
|
||||
return
|
||||
}
|
||||
console.log(
|
||||
`[Run core main] An internal error is caught, and browser will be restarted in ${rerunInterval}ms.`
|
||||
)
|
||||
@@ -141,14 +166,32 @@ const runAutoChat = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const waitForProcessHandShakeAndRunAutoChat = () => {
|
||||
let pipe: null | fs.WriteStream = null
|
||||
try {
|
||||
pipe = fs.createWriteStream(null, { fd: 3 })
|
||||
} catch {
|
||||
console.error('pipe is not available')
|
||||
app.exit(1)
|
||||
}
|
||||
export const waitForProcessHandShakeAndRunAutoChat = async () => {
|
||||
await app.whenReady()
|
||||
app.on('window-all-closed', (e) => {
|
||||
e.preventDefault()
|
||||
})
|
||||
initPublicIpc()
|
||||
await connectToDaemon()
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'ping'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'worker-launch',
|
||||
status: 'fulfilled'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
runAutoChat()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,26 @@ import {
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import {
|
||||
RECOMMEND_JOB_ENTRY_SELECTOR,
|
||||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs'
|
||||
import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
|
||||
import {
|
||||
saveJobInfoFromRecommendPage,
|
||||
saveChatStartupRecord,
|
||||
saveMarkAsNotSuitRecord,
|
||||
saveChatMessageRecord
|
||||
saveChatMessageRecord,
|
||||
saveJobHireStatusRecord
|
||||
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||
import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import {
|
||||
MarkAsNotSuitReason,
|
||||
JobSource,
|
||||
JobHireStatus
|
||||
} from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import { Target } from 'puppeteer'
|
||||
@@ -39,13 +49,136 @@ const attachRequestsListener = async (target: Target) => {
|
||||
return
|
||||
}
|
||||
|
||||
function handleJobDetailPage({ encryptJobId } = { encryptJobId: null }) {
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
if (!encryptJobId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
({ encryptJobId }) => {
|
||||
return (
|
||||
location.href.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
|
||||
document.readyState === 'complete'
|
||||
)
|
||||
},
|
||||
undefined,
|
||||
{ encryptJobId }
|
||||
)
|
||||
const htmlContent = await page.content()
|
||||
if (htmlContent) {
|
||||
const $ = cheerio.load(htmlContent)
|
||||
const [jobBannerEl] = $('#main .job-banner') ?? []
|
||||
if (!jobBannerEl) {
|
||||
console.log(`access might be blocked`)
|
||||
if (
|
||||
htmlContent.includes(`您访问的页面不存在`) ||
|
||||
location.href === `https://www.zhipin.com/`
|
||||
) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.DELETED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
|
||||
if (jobStatusTextEl) {
|
||||
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
|
||||
if ([`职位已关闭`].includes(jobStatusText)) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.CLOSED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
} else {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
})
|
||||
.catch(() => void 0)
|
||||
}
|
||||
|
||||
if (page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
|
||||
const encryptJobId = page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
|
||||
if (encryptJobId) {
|
||||
handleJobDetailPage({ encryptJobId })
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentJobSource() {
|
||||
const methodMap = {
|
||||
async recommend() {
|
||||
return await page.evaluate(
|
||||
({ RECOMMEND_JOB_ENTRY_SELECTOR }) => {
|
||||
return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active')
|
||||
},
|
||||
{
|
||||
RECOMMEND_JOB_ENTRY_SELECTOR
|
||||
}
|
||||
)
|
||||
},
|
||||
async expect() {
|
||||
return await page.evaluate(
|
||||
({ USER_SET_EXPECT_JOB_ENTRIES_SELECTOR }) => {
|
||||
return [...document.querySelectorAll(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)].some((el) =>
|
||||
el.classList.contains('active')
|
||||
)
|
||||
},
|
||||
{
|
||||
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR
|
||||
}
|
||||
)
|
||||
},
|
||||
async search() {
|
||||
const elHandle = await page.$(`.page-jobs-main`)
|
||||
const currentKeyWord = await elHandle?.evaluate((el) => {
|
||||
return el?.__vue__?.formData?.query
|
||||
})
|
||||
return !!currentKeyWord
|
||||
}
|
||||
}
|
||||
for (const [type, func] of Object.entries(methodMap)) {
|
||||
try {
|
||||
if (await func()) {
|
||||
return type
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('encounter error when get job source')
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
page.on('response', async (response) => {
|
||||
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
|
||||
if (response.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
|
||||
const encryptJobId = response
|
||||
.url()
|
||||
.match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
|
||||
if (encryptJobId) {
|
||||
handleJobDetailPage({ encryptJobId })
|
||||
}
|
||||
} else if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
|
||||
const data = await response.json()
|
||||
|
||||
console.log(data)
|
||||
if (data.code === 0) {
|
||||
await saveJobInfoFromRecommendPage(await dbInitPromise, data.zpData)
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId: data.zpData.jobInfo.encryptId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/reasons.json')
|
||||
@@ -81,7 +214,7 @@ const attachRequestsListener = async (target: Target) => {
|
||||
const reasonCodeToTextMap = await readStorageFile(
|
||||
'job-not-suit-reason-code-to-text-cache.json'
|
||||
)
|
||||
|
||||
const jobSource = await getCurrentJobSource()
|
||||
const markDetail = {
|
||||
markFrom: ChatStartupFrom.ManuallyFromRecommendList,
|
||||
extInfo: {
|
||||
@@ -90,9 +223,15 @@ const attachRequestsListener = async (target: Target) => {
|
||||
text: reasonCodeToTextMap[chosenCode]
|
||||
}
|
||||
},
|
||||
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON
|
||||
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON,
|
||||
jobSource: JobSource[jobSource]
|
||||
}
|
||||
gtag('job_marked_as_not_suit', { markFrom: markDetail.markFrom })
|
||||
gtag('job_marked_as_not_suit', {
|
||||
markFrom: markDetail.markFrom,
|
||||
bossActiveTimeDesc: currentJobData?.bossInfo?.activeTimeDesc,
|
||||
encryptJobId: currentJobData?.jobInfo?.encryptId,
|
||||
jobSource: JobSource[jobSource]
|
||||
})
|
||||
if (reasonCodeToTextMap[chosenCode]?.includes('活跃度低')) {
|
||||
markDetail.markReason = MarkAsNotSuitReason.BOSS_INACTIVE
|
||||
markDetail.extInfo.bossActiveTimeDesc = currentJobData?.bossInfo.activeTimeDesc
|
||||
@@ -127,7 +266,12 @@ const attachRequestsListener = async (target: Target) => {
|
||||
const currentUserInfo = await page.evaluate(
|
||||
'document.querySelector(".job-detail-box").__vue__.$store.state.userInfo'
|
||||
)
|
||||
gtag('new_chat_startup', { chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList })
|
||||
const jobSource = await getCurrentJobSource()
|
||||
gtag('new_chat_startup', {
|
||||
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
|
||||
encryptJobId: currentJobData?.jobInfo?.encryptId,
|
||||
jobSource: JobSource[jobSource]
|
||||
})
|
||||
await saveChatStartupRecord(
|
||||
await dbInitPromise,
|
||||
currentJobData,
|
||||
@@ -135,7 +279,8 @@ const attachRequestsListener = async (target: Target) => {
|
||||
encryptUserId: currentUserInfo.encryptUserId
|
||||
},
|
||||
{
|
||||
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList
|
||||
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
|
||||
jobSource: JobSource[jobSource]
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
@@ -184,11 +329,15 @@ const attachRequestsListener = async (target: Target) => {
|
||||
)
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const chatRecordList = rawChatRecordList.map(it => {
|
||||
const chatRecordList = rawChatRecordList.map((it) => {
|
||||
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
|
||||
mappedItem.mid = it.mid
|
||||
mappedItem.encryptFromUserId = it.isSelf ? currentUserInfo.encryptUserId : bossInfo.encryptBossId
|
||||
mappedItem.encryptToUserId = it.isSelf ? bossInfo.encryptBossId : currentUserInfo.encryptUserId
|
||||
mappedItem.encryptFromUserId = it.isSelf
|
||||
? currentUserInfo.encryptUserId
|
||||
: bossInfo.encryptBossId
|
||||
mappedItem.encryptToUserId = it.isSelf
|
||||
? bossInfo.encryptBossId
|
||||
: currentUserInfo.encryptUserId
|
||||
mappedItem.style = it.isSelf ? 'sent' : 'received'
|
||||
mappedItem.type = it.type
|
||||
mappedItem.time = it.time ? new Date(it.time) : null
|
||||
@@ -222,7 +371,8 @@ export async function launchBossSite() {
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: [`--load-extension=${editThisCookieExtensionPath}`]
|
||||
pipe: true,
|
||||
enableExtensions: [editThisCookieExtensionPath]
|
||||
})
|
||||
let [page] = await browser.pages()
|
||||
for (let i = 0; i < bossCookies.length; i++) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { app } from 'electron'
|
||||
import { main, loginEventBus } from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
|
||||
import { app, dialog } from 'electron'
|
||||
import {
|
||||
main,
|
||||
loginEventBus
|
||||
} from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
|
||||
import { pipeWriteRegardlessError } from './utils/pipe'
|
||||
import fs from "node:fs";
|
||||
import fs from 'node:fs'
|
||||
import { getLastUsedAndAvailableBrowser } from './DOWNLOAD_DEPENDENCIES/utils/browser-history'
|
||||
import { configWithBrowserAssistant } from '../features/config-with-browser-assistant'
|
||||
|
||||
export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
|
||||
process.on('disconnect', () => app.exit())
|
||||
@@ -18,15 +23,26 @@ export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
|
||||
type: 'INITIALIZE_PUPPETEER'
|
||||
}) + '\r\n'
|
||||
)
|
||||
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
if (!puppeteerExecutable) {
|
||||
try {
|
||||
await configWithBrowserAssistant({ autoFind: true })
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
}
|
||||
if (!puppeteerExecutable) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `未找到可用的浏览器`,
|
||||
detail: `请重新运行本程序,按照提示安装、配置浏览器`
|
||||
})
|
||||
app.exit(1)
|
||||
}
|
||||
const { initPuppeteer } = await import('@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs')
|
||||
try {
|
||||
await initPuppeteer()
|
||||
pipeWriteRegardlessError(
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
|
||||
}) + '\r\n'
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
app.exit(1)
|
||||
|
||||
5
packages/ui/src/main/flow/LAUNCH_DAEMON.ts
Normal file
5
packages/ui/src/main/flow/LAUNCH_DAEMON.ts
Normal file
@@ -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 gtag from '../../utils/gtag'
|
||||
import initPublicIpc from '../../utils/initPublicIpc'
|
||||
import { sendToDaemon, closeDaemonClient } from './connect-to-daemon'
|
||||
|
||||
export function openSettingWindow() {
|
||||
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
@@ -65,4 +67,25 @@ export function openSettingWindow() {
|
||||
globalShortcut.unregister('Command+Option+Shift+/')
|
||||
})
|
||||
})
|
||||
|
||||
whenReadyPromise.then(async () => {
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'ping'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'user-process-register'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
})
|
||||
app.on('window-all-closed', closeDaemonClient)
|
||||
app.on('before-quit', closeDaemonClient)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { ipcMain, shell, app } from 'electron'
|
||||
import { ipcMain, shell, app, dialog, BrowserWindow } from 'electron'
|
||||
import path from 'path'
|
||||
import * as childProcess from 'node:child_process'
|
||||
import {
|
||||
ensureConfigFileExist,
|
||||
ensureStorageFileExist,
|
||||
configFileNameList,
|
||||
readConfigFile,
|
||||
writeConfigFile,
|
||||
readStorageFile,
|
||||
writeStorageFile,
|
||||
storageFilePath
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { ChildProcess } from 'child_process'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
import { checkCookieListFormat } from '../../../../common/utils/cookie'
|
||||
import { getAnyAvailablePuppeteerExecutable } from '../../../flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../../common/enums/auto-start-chat'
|
||||
import { getAnyAvailablePuppeteerExecutable } from '../../DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index'
|
||||
import { mainWindow } from '../../../window/mainWindow'
|
||||
import {
|
||||
getAutoStartChatRecord,
|
||||
@@ -36,11 +32,11 @@ import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/re
|
||||
import {
|
||||
getValidTemplate,
|
||||
requestNewMessageContent
|
||||
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
|
||||
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
|
||||
import {
|
||||
autoReminderPromptTemplateFileName,
|
||||
writeDefaultAutoRemindPrompt
|
||||
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
|
||||
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
|
||||
import {
|
||||
checkIsResumeContentValid,
|
||||
resumeContentEnoughDetect
|
||||
@@ -52,6 +48,16 @@ import {
|
||||
import { RequestSceneEnum } from '../../../features/llm-request-log'
|
||||
import { checkUpdateForUi } from '../../../features/updater'
|
||||
import gtag from '../../../utils/gtag'
|
||||
import { daemonEE, sendToDaemon } from '../connect-to-daemon'
|
||||
import { runCommon } from '../../../features/run-common'
|
||||
import { loginWithCookieAssistant } from '../../../features/login-with-cookie-assistant'
|
||||
import { configWithBrowserAssistant } from '../../../features/config-with-browser-assistant'
|
||||
import {
|
||||
createFirstLaunchNoticeApproveFlag,
|
||||
isFirstLaunchNoticeApproveFlagExist,
|
||||
waitForUserApproveAgreement
|
||||
} from '../../../features/first-launch-notice-window'
|
||||
import { getLastUsedAndAvailableBrowser } from '../../DOWNLOAD_DEPENDENCIES/utils/browser-history'
|
||||
|
||||
export default function initIpc() {
|
||||
ipcMain.handle('fetch-config-file-content', async () => {
|
||||
@@ -95,9 +101,93 @@ export default function initIpc() {
|
||||
if (hasOwn(payload, 'expectJobDescRegExpStr')) {
|
||||
bossConfig.expectJobDescRegExpStr = payload.expectJobDescRegExpStr
|
||||
}
|
||||
if (hasOwn(payload, 'jobNotMatchStrategy')) {
|
||||
bossConfig.jobNotMatchStrategy = payload.jobNotMatchStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'markAsNotActiveSelectedTimeRange')) {
|
||||
bossConfig.markAsNotActiveSelectedTimeRange = payload.markAsNotActiveSelectedTimeRange
|
||||
}
|
||||
if (hasOwn(payload, 'jobNotActiveStrategy')) {
|
||||
bossConfig.jobNotActiveStrategy = payload.jobNotActiveStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'autoReminder')) {
|
||||
bossConfig.autoReminder = payload.autoReminder
|
||||
}
|
||||
|
||||
// city
|
||||
if (hasOwn(payload, 'expectCityList')) {
|
||||
bossConfig.expectCityList = payload.expectCityList
|
||||
}
|
||||
if (hasOwn(payload, 'expectCityNotMatchStrategy')) {
|
||||
bossConfig.expectCityNotMatchStrategy = payload.expectCityNotMatchStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'strategyScopeOptionWhenMarkJobCityNotMatch')) {
|
||||
bossConfig.strategyScopeOptionWhenMarkJobCityNotMatch =
|
||||
payload.strategyScopeOptionWhenMarkJobCityNotMatch
|
||||
}
|
||||
|
||||
// salary
|
||||
if (hasOwn(payload, 'expectSalaryCalculateWay')) {
|
||||
bossConfig.expectSalaryCalculateWay = payload.expectSalaryCalculateWay
|
||||
}
|
||||
if (hasOwn(payload, 'expectSalaryNotMatchStrategy')) {
|
||||
bossConfig.expectSalaryNotMatchStrategy = payload.expectSalaryNotMatchStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'strategyScopeOptionWhenMarkSalaryNotMatch')) {
|
||||
bossConfig.strategyScopeOptionWhenMarkSalaryNotMatch =
|
||||
payload.strategyScopeOptionWhenMarkSalaryNotMatch
|
||||
}
|
||||
if (hasOwn(payload, 'expectSalaryLow')) {
|
||||
bossConfig.expectSalaryLow = payload.expectSalaryLow
|
||||
}
|
||||
if (hasOwn(payload, 'expectSalaryHigh')) {
|
||||
bossConfig.expectSalaryHigh = payload.expectSalaryHigh
|
||||
}
|
||||
|
||||
// work exp
|
||||
if (hasOwn(payload, 'expectWorkExpList')) {
|
||||
bossConfig.expectWorkExpList = payload.expectWorkExpList
|
||||
}
|
||||
if (hasOwn(payload, 'expectWorkExpNotMatchStrategy')) {
|
||||
bossConfig.expectWorkExpNotMatchStrategy = payload.expectWorkExpNotMatchStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'strategyScopeOptionWhenMarkJobWorkExpNotMatch')) {
|
||||
bossConfig.strategyScopeOptionWhenMarkJobWorkExpNotMatch =
|
||||
payload.strategyScopeOptionWhenMarkJobWorkExpNotMatch
|
||||
}
|
||||
if (hasOwn(payload, 'jobDetailRegExpMatchLogic')) {
|
||||
bossConfig.jobDetailRegExpMatchLogic = payload.jobDetailRegExpMatchLogic
|
||||
}
|
||||
if (hasOwn(payload, 'isSkipEmptyConditionForCombineRecommendJobFilter')) {
|
||||
bossConfig.isSkipEmptyConditionForCombineRecommendJobFilter =
|
||||
payload.isSkipEmptyConditionForCombineRecommendJobFilter
|
||||
}
|
||||
if (hasOwn(payload, 'jobSourceList')) {
|
||||
bossConfig.jobSourceList = payload.jobSourceList
|
||||
}
|
||||
if (hasOwn(payload, 'combineRecommendJobFilterType')) {
|
||||
bossConfig.combineRecommendJobFilterType = payload.combineRecommendJobFilterType
|
||||
}
|
||||
if (hasOwn(payload, 'staticCombineRecommendJobFilterConditions')) {
|
||||
bossConfig.staticCombineRecommendJobFilterConditions =
|
||||
payload.staticCombineRecommendJobFilterConditions
|
||||
}
|
||||
if (hasOwn(payload, 'isSageTimeEnabled')) {
|
||||
bossConfig.isSageTimeEnabled = payload.isSageTimeEnabled
|
||||
}
|
||||
if (hasOwn(payload, 'sageTimeOpTimes')) {
|
||||
bossConfig.sageTimeOpTimes = payload.sageTimeOpTimes
|
||||
}
|
||||
if (hasOwn(payload, 'sageTimePauseMinute')) {
|
||||
bossConfig.sageTimePauseMinute = payload.sageTimePauseMinute
|
||||
}
|
||||
if (hasOwn(payload, 'blockCompanyNameRegExpStr')) {
|
||||
bossConfig.blockCompanyNameRegExpStr = payload.blockCompanyNameRegExpStr
|
||||
}
|
||||
if (hasOwn(payload, 'blockCompanyNameRegMatchStrategy')) {
|
||||
bossConfig.blockCompanyNameRegMatchStrategy = payload.blockCompanyNameRegMatchStrategy
|
||||
}
|
||||
|
||||
promiseArr.push(writeConfigFile('boss.json', bossConfig))
|
||||
|
||||
if (hasOwn(payload, 'expectCompanies')) {
|
||||
@@ -109,262 +199,111 @@ export default function initIpc() {
|
||||
return await Promise.all(promiseArr)
|
||||
})
|
||||
|
||||
ipcMain.handle('read-storage-file', async (ev, payload) => {
|
||||
ensureStorageFileExist()
|
||||
return await readStorageFile(payload.fileName)
|
||||
})
|
||||
|
||||
ipcMain.handle('write-storage-file', async (ev, payload) => {
|
||||
ensureStorageFileExist()
|
||||
|
||||
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
|
||||
})
|
||||
|
||||
// const currentExecutablePath = app.getPath('exe')
|
||||
// console.log(currentExecutablePath)
|
||||
ipcMain.handle('prepare-run-geek-auto-start-chat-with-boss', async () => {
|
||||
mainWindow?.webContents.send('locating-puppeteer-executable')
|
||||
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
|
||||
if (!puppeteerExecutable) {
|
||||
return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
|
||||
}
|
||||
mainWindow?.webContents.send('puppeteer-executable-is-located')
|
||||
})
|
||||
|
||||
let subProcessOfPuppeteer: ChildProcess | null = null
|
||||
ipcMain.handle('run-geek-auto-start-chat-with-boss', async () => {
|
||||
if (subProcessOfPuppeteer) {
|
||||
return
|
||||
}
|
||||
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
|
||||
if (!puppeteerExecutable) {
|
||||
return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
|
||||
}
|
||||
const subProcessEnv = {
|
||||
...process.env,
|
||||
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'geekAutoStartWithBossDaemon',
|
||||
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
|
||||
}
|
||||
subProcessOfPuppeteer = childProcess.spawn(
|
||||
process.argv[0],
|
||||
[...process.argv.slice(1), '--mode-to-daemon=geekAutoStartWithBossMain'],
|
||||
{
|
||||
env: subProcessEnv,
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
|
||||
ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => {
|
||||
const mode = 'geekAutoStartWithBossMain'
|
||||
const { runRecordId } = await runCommon({ mode })
|
||||
daemonEE.on('message', function handler(message) {
|
||||
if (message.workerId !== mode) {
|
||||
return
|
||||
}
|
||||
if (message.type === 'worker-exited') {
|
||||
mainWindow?.webContents.send('worker-exited', message)
|
||||
}
|
||||
)
|
||||
// console.log(subProcessOfPuppeteer)
|
||||
return new Promise((resolve, reject) => {
|
||||
subProcessOfPuppeteer!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => {
|
||||
const data = raw
|
||||
switch (data.type) {
|
||||
case 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED': {
|
||||
resolve(data)
|
||||
break
|
||||
}
|
||||
case 'LOGIN_STATUS_INVALID': {
|
||||
await sleep(500)
|
||||
mainWindow?.webContents.send('check-boss-zhipin-cookie-file')
|
||||
return
|
||||
}
|
||||
default: {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
subProcessOfPuppeteer!.once('exit', (exitCode) => {
|
||||
subProcessOfPuppeteer = null
|
||||
if (exitCode === AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) {
|
||||
// means cannot find downloaded puppeteer
|
||||
reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
|
||||
} else {
|
||||
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
|
||||
}
|
||||
})
|
||||
})
|
||||
// TODO:
|
||||
return { runRecordId }
|
||||
})
|
||||
|
||||
ipcMain.handle('run-read-no-reply-auto-reminder', async () => {
|
||||
if (subProcessOfPuppeteer) {
|
||||
return
|
||||
}
|
||||
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
|
||||
if (!puppeteerExecutable) {
|
||||
return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
|
||||
}
|
||||
const subProcessEnv = {
|
||||
...process.env,
|
||||
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'readNoReplyAutoReminder',
|
||||
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
|
||||
}
|
||||
subProcessOfPuppeteer = childProcess.spawn(process.argv[0], process.argv.slice(1), {
|
||||
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 '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
|
||||
}
|
||||
const subProcessEnv = {
|
||||
...process.env,
|
||||
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'checkAndDownloadDependenciesForInit'
|
||||
}
|
||||
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
|
||||
process.argv[0],
|
||||
process.argv.slice(1),
|
||||
{
|
||||
env: subProcessEnv,
|
||||
stdio: [null, null, null, 'pipe', 'ipc']
|
||||
const mode = 'readNoReplyAutoReminderMain'
|
||||
const { runRecordId } = await runCommon({ mode })
|
||||
daemonEE.on('message', function handler(message) {
|
||||
if (message.workerId !== mode) {
|
||||
return
|
||||
}
|
||||
if (message.type === 'worker-exited') {
|
||||
mainWindow?.webContents.send('worker-exited', message)
|
||||
}
|
||||
)
|
||||
return new Promise((resolve, reject) => {
|
||||
subProcessOfCheckAndDownloadDependencies!.stdio[3]!.pipe(JSONStream.parse()).on(
|
||||
'data',
|
||||
(raw) => {
|
||||
const data = raw
|
||||
switch (data.type) {
|
||||
case 'NEED_RESETUP_DEPENDENCIES':
|
||||
case 'PUPPETEER_DOWNLOAD_PROGRESS': {
|
||||
mainWindow?.webContents.send(data.type, data)
|
||||
break
|
||||
}
|
||||
case 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR': {
|
||||
console.error(data)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
subProcessOfCheckAndDownloadDependencies!.once('exit', (exitCode) => {
|
||||
switch (exitCode) {
|
||||
case 0: {
|
||||
resolve(exitCode)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
reject('PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR')
|
||||
break
|
||||
}
|
||||
}
|
||||
subProcessOfCheckAndDownloadDependencies = null
|
||||
})
|
||||
})
|
||||
return { runRecordId }
|
||||
})
|
||||
|
||||
ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => {
|
||||
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping')
|
||||
subProcessOfPuppeteer?.kill()
|
||||
setTimeout(() => {
|
||||
try {
|
||||
subProcessOfPuppeteer?.kill('SIGKILL')
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null
|
||||
ipcMain.on('launch-bosszhipin-login-page-with-preload-extension', async () => {
|
||||
try {
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
const subProcessEnv = {
|
||||
...process.env,
|
||||
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'launchBossZhipinLoginPageWithPreloadExtension',
|
||||
PUPPETEER_EXECUTABLE_PATH: (await getAnyAvailablePuppeteerExecutable())!.executablePath
|
||||
}
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
|
||||
process.argv[0],
|
||||
process.argv.slice(1),
|
||||
{
|
||||
env: subProcessEnv,
|
||||
stdio: [null, null, null, 'pipe', 'ipc']
|
||||
}
|
||||
)
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension!.stdio[3]!.pipe(JSONStream.parse()).on(
|
||||
'data',
|
||||
(raw) => {
|
||||
const data = raw
|
||||
switch (data.type) {
|
||||
case 'BOSS_ZHIPIN_COOKIE_COLLECTED': {
|
||||
mainWindow?.webContents.send(data.type, data)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return
|
||||
}
|
||||
const p = new Promise((resolve) => {
|
||||
daemonEE.on('message', function handler(message) {
|
||||
if (message.workerId !== 'geekAutoStartWithBossMain') {
|
||||
return
|
||||
}
|
||||
if (message.type === 'worker-exited') {
|
||||
daemonEE.off('message', handler)
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'stop-worker',
|
||||
workerId: 'geekAutoStartWithBossMain'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension!.once('exit', () => {
|
||||
mainWindow?.webContents.send('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED')
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
|
||||
})
|
||||
await p
|
||||
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
|
||||
})
|
||||
ipcMain.on('kill-bosszhipin-login-page-with-preload-extension', async () => {
|
||||
try {
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
|
||||
} catch {
|
||||
//
|
||||
} finally {
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
|
||||
}
|
||||
|
||||
ipcMain.handle('stop-read-no-reply-auto-reminder', async () => {
|
||||
mainWindow?.webContents.send('read-no-reply-auto-reminder-stopping')
|
||||
const p = new Promise((resolve) => {
|
||||
daemonEE.on('message', function handler(message) {
|
||||
if (message.workerId !== 'readNoReplyAutoReminderMain') {
|
||||
return
|
||||
}
|
||||
if (message.type === 'worker-exited') {
|
||||
daemonEE.off('message', handler)
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'stop-worker',
|
||||
workerId: 'readNoReplyAutoReminderMain'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
|
||||
await p
|
||||
mainWindow?.webContents.send('read-no-reply-auto-reminder-stopped')
|
||||
})
|
||||
|
||||
ipcMain.handle('get-task-manager-list', async () => {
|
||||
const result = await sendToDaemon(
|
||||
{
|
||||
type: 'get-status'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
return result
|
||||
})
|
||||
|
||||
// IPC处理:停止工具进程
|
||||
ipcMain.handle('stop-task', async (_, workerId) => {
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'stop-worker',
|
||||
workerId
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
ipcMain.handle('check-boss-zhipin-cookie-file', () => {
|
||||
@@ -395,7 +334,7 @@ export default function initIpc() {
|
||||
|
||||
let subProcessOfOpenBossSiteDefer: null | PromiseWithResolvers<ChildProcess> = null
|
||||
let subProcessOfOpenBossSite: null | ChildProcess = null
|
||||
ipcMain.handle('open-site-with-boss-cookie', async (_, data) => {
|
||||
ipcMain.handle('open-site-with-boss-cookie', async (ev, data) => {
|
||||
const url = data.url
|
||||
if (
|
||||
!subProcessOfOpenBossSiteDefer ||
|
||||
@@ -403,16 +342,43 @@ export default function initIpc() {
|
||||
subProcessOfOpenBossSite.killed
|
||||
) {
|
||||
subProcessOfOpenBossSiteDefer = Promise.withResolvers()
|
||||
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
|
||||
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
if (!puppeteerExecutable) {
|
||||
try {
|
||||
const parent = BrowserWindow.fromWebContents(ev.sender) || undefined
|
||||
await configWithBrowserAssistant({
|
||||
autoFind: true,
|
||||
windowOption: {
|
||||
parent,
|
||||
modal: !!parent,
|
||||
show: true
|
||||
}
|
||||
})
|
||||
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
if (!puppeteerExecutable) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `未找到可用的浏览器`,
|
||||
detail: `请重新运行本程序,按照提示安装、配置浏览器`
|
||||
})
|
||||
return
|
||||
}
|
||||
const subProcessEnv = {
|
||||
...process.env,
|
||||
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'launchBossSite',
|
||||
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable!.executablePath
|
||||
}
|
||||
subProcessOfOpenBossSite = childProcess.spawn(process.argv[0], process.argv.slice(1), {
|
||||
env: subProcessEnv,
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'pipe']
|
||||
})
|
||||
subProcessOfOpenBossSite = childProcess.spawn(
|
||||
process.argv[0],
|
||||
[process.argv[1], `--mode=launchBossSite`],
|
||||
{
|
||||
env: subProcessEnv,
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'pipe']
|
||||
}
|
||||
)
|
||||
subProcessOfOpenBossSite.once('exit', () => {
|
||||
subProcessOfOpenBossSiteDefer = null
|
||||
})
|
||||
@@ -501,16 +467,15 @@ export default function initIpc() {
|
||||
ipcMain.handle('save-resume-content', saveResumeHandler)
|
||||
resumeEditorWindow?.once('closed', () => {
|
||||
ipcMain.removeHandler('save-resume-content')
|
||||
ipcMain.removeHandler('fetch-resume-content')
|
||||
defer.reject(new Error('cancel'))
|
||||
})
|
||||
|
||||
ipcMain.handle('fetch-resume-content', async () => {
|
||||
const res = (await readConfigFile('resumes.json'))?.[0]
|
||||
return res?.content ?? null
|
||||
})
|
||||
return defer.promise
|
||||
})
|
||||
ipcMain.handle('fetch-resume-content', async () => {
|
||||
const res = (await readConfigFile('resumes.json'))?.[0]
|
||||
return res?.content ?? null
|
||||
})
|
||||
ipcMain.on('no-reply-reminder-prompt-edit', async () => {
|
||||
const template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false })
|
||||
if (!template) {
|
||||
@@ -586,6 +551,60 @@ export default function initIpc() {
|
||||
const newRelease = await checkUpdateForUi()
|
||||
return newRelease
|
||||
})
|
||||
ipcMain.handle('login-with-cookie-assistant', async () => {
|
||||
return await loginWithCookieAssistant({
|
||||
windowOption: {
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
}
|
||||
})
|
||||
})
|
||||
ipcMain.handle('config-with-browser-assistant', async () => {
|
||||
return await configWithBrowserAssistant({
|
||||
windowOption: {
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('pre-enter-setting-ui', async () => {
|
||||
if (!isFirstLaunchNoticeApproveFlagExist()) {
|
||||
try {
|
||||
await waitForUserApproveAgreement({
|
||||
windowOption: {
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
}
|
||||
})
|
||||
createFirstLaunchNoticeApproveFlag()
|
||||
} catch {
|
||||
app.exit(0)
|
||||
return
|
||||
}
|
||||
}
|
||||
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
|
||||
if (!puppeteerExecutable) {
|
||||
const lastBrowser = await getLastUsedAndAvailableBrowser()
|
||||
if (!lastBrowser) {
|
||||
try {
|
||||
await configWithBrowserAssistant({
|
||||
windowOption: {
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
},
|
||||
autoFind: true
|
||||
})
|
||||
} catch (err) {
|
||||
void err
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('exit-app-immediately', () => {
|
||||
app.exit(0)
|
||||
|
||||
@@ -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
|
||||
worker?.off('message', handler)
|
||||
} else if (data.type === 'DB_INIT_FAIL') {
|
||||
reject(undefined)
|
||||
reject(data.error)
|
||||
worker?.terminate()
|
||||
worker?.off('message', handler)
|
||||
worker = null
|
||||
@@ -102,3 +102,10 @@ export const getJobHistoryByEncryptId = async (encryptJobId) => {
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
export const saveAndGetCurrentRunRecord = async () => {
|
||||
const res = await createWorkerPromise({
|
||||
type: 'saveAndGetCurrentRunRecord'
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { VMarkAsNotSuitLog } from '@geekgeekrun/sqlite-plugin/dist/entity/VMarkA
|
||||
import { measureExecutionTime } from '../../../../../../common/utils/performance'
|
||||
import { PageReq, PagedRes } from '../../../../../../common/types/pagination'
|
||||
import { JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/dist/entity/JobInfoChangeLog'
|
||||
import { AutoStartChatRunRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/AutoStartChatRunRecord'
|
||||
|
||||
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||
let dataSource: DataSource | null = null
|
||||
@@ -161,6 +162,13 @@ const payloadHandler = {
|
||||
})
|
||||
)
|
||||
return data
|
||||
},
|
||||
async saveAndGetCurrentRunRecord() {
|
||||
const autoStartChatRunRecord = new AutoStartChatRunRecord()
|
||||
autoStartChatRunRecord.date = new Date()
|
||||
const autoStartChatRunRecordRepository = dataSource!.getRepository(AutoStartChatRunRecord)
|
||||
const result = await autoStartChatRunRecordRepository.save(autoStartChatRunRecord)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
import { bootstrap, launchBoss } from './bootstrap'
|
||||
import { MsgStatus, type ChatListItem } from './types'
|
||||
import { Browser, Page } from 'puppeteer'
|
||||
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
|
||||
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||
import { app, dialog } from 'electron'
|
||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||
import {
|
||||
getPublicDbFilePath,
|
||||
readConfigFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatMessageRecord'
|
||||
import { saveChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
import { writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import * as fs from 'fs'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||
import { RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat'
|
||||
import gtag from '../../utils/gtag'
|
||||
|
||||
const throttleIntervalMinutes =
|
||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||
const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21
|
||||
const recentMessageQuantityForLlm =
|
||||
readConfigFile('boss.json').autoReminder?.recentMessageQuantityForLlm ?? 8
|
||||
const rechatContentSource =
|
||||
readConfigFile('boss.json').autoReminder?.rechatContentSource ??
|
||||
RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION
|
||||
const rechatLlmFallback =
|
||||
readConfigFile('boss.json').autoReminder?.rechatLlmFallback ??
|
||||
RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||
|
||||
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||
|
||||
export const pageMapByName: {
|
||||
boss?: Page | null
|
||||
} = {}
|
||||
|
||||
async function saveCurrentChatRecord(page) {
|
||||
const userInfo = await page.evaluate(
|
||||
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
|
||||
)
|
||||
const bossInfo = await page.evaluate(
|
||||
'document.querySelector(".chat-conversation .chat-record")?.__vue__?.boss'
|
||||
)
|
||||
|
||||
const ds = await dbInitPromise
|
||||
// save boss info
|
||||
const bossInfoRepository = ds.getRepository(BossInfo)
|
||||
let targetBossInfo = await bossInfoRepository.findOneBy({
|
||||
encryptBossId: bossInfo.encryptBossId
|
||||
})
|
||||
if (!targetBossInfo) {
|
||||
targetBossInfo = new BossInfo()
|
||||
Object.assign(targetBossInfo, {
|
||||
encryptBossId: bossInfo.encryptBossId,
|
||||
name: bossInfo.name,
|
||||
title: bossInfo.title,
|
||||
date: new Date()
|
||||
})
|
||||
await bossInfoRepository.save(targetBossInfo)
|
||||
}
|
||||
|
||||
const rawChatRecordList =
|
||||
(
|
||||
await page.evaluate(
|
||||
'document.querySelector(".message-content .chat-record").__vue__.records$'
|
||||
)
|
||||
)?.filter((msg) => ['received', 'sent'].includes(msg.style)) ?? []
|
||||
const chatRecordList = rawChatRecordList.map((it) => {
|
||||
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
|
||||
mappedItem.mid = it.mid
|
||||
mappedItem.encryptFromUserId =
|
||||
it.style === 'sent'
|
||||
? userInfo.encryptUserId
|
||||
: it.style === 'received'
|
||||
? bossInfo.encryptBossId
|
||||
: ''
|
||||
mappedItem.encryptToUserId =
|
||||
it.style === 'sent'
|
||||
? bossInfo.encryptBossId
|
||||
: it.style === 'received'
|
||||
? userInfo.encryptUserId
|
||||
: ''
|
||||
mappedItem.style = it.style
|
||||
mappedItem.type = it.type
|
||||
mappedItem.time = it.time ? new Date(it.time) : null
|
||||
mappedItem.text = it.text
|
||||
if (it.type === 'image') {
|
||||
mappedItem.imageUrl = it.image?.originImage?.url
|
||||
mappedItem.imageHeight = it.image?.originImage?.url?.height
|
||||
mappedItem.imageWidth = it.image?.originImage?.url?.width
|
||||
}
|
||||
|
||||
return mappedItem
|
||||
})
|
||||
|
||||
await saveChatMessageRecord(ds, chatRecordList)
|
||||
}
|
||||
|
||||
let browser: null | Browser = null
|
||||
const mainLoop = async () => {
|
||||
if (browser) {
|
||||
try {
|
||||
const cp = browser.process()
|
||||
cp?.kill('SIGKILL')
|
||||
} catch {
|
||||
//
|
||||
} finally {
|
||||
browser = null
|
||||
}
|
||||
}
|
||||
const canNotConfirmIfHasReadMsgTemplateList = [
|
||||
'Boss还没查看你的消息',
|
||||
'你与该职位竞争者PK情况',
|
||||
'简历诊断提醒',
|
||||
'附件简历还没准备好',
|
||||
'开场问题,期待你的回答',
|
||||
'设置合适的期望薪资范围'
|
||||
].map((it) => new RegExp(it))
|
||||
browser = await bootstrap()
|
||||
await Promise.all([launchBoss(browser)])
|
||||
|
||||
await sleep(1000)
|
||||
pageMapByName.boss!.bringToFront()
|
||||
await sleep(2000)
|
||||
|
||||
const currentPageUrl = pageMapByName.boss!.url() ?? ''
|
||||
// #region
|
||||
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
|
||||
writeStorageFile('boss-cookies.json', [])
|
||||
throw new Error('LOGIN_STATUS_INVALID')
|
||||
}
|
||||
if (
|
||||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/403.html') ||
|
||||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/error.html')
|
||||
) {
|
||||
throw new Error('ACCESS_IS_DENIED')
|
||||
}
|
||||
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/safe/verify-slider')) {
|
||||
const validateRes: any = await pageMapByName
|
||||
.boss!.waitForResponse(
|
||||
(response) => {
|
||||
if (
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpAntispam/v2/geetest/validate')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
{
|
||||
timeout: 0
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
return res.json()
|
||||
})
|
||||
if (validateRes.code === 0) {
|
||||
await storeStorage(pageMapByName.boss)
|
||||
throw new Error('CAPTCHA_PASSED_AND_NEED_RESTART')
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
// check set security question tip modal
|
||||
let setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$(
|
||||
'.dialog-wrap.dialog-account-safe'
|
||||
)
|
||||
if (setSecurityQuestionTipModelProxy) {
|
||||
await sleep(1000)
|
||||
setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$(
|
||||
'.dialog-wrap.dialog-account-safe'
|
||||
)
|
||||
const closeButtonProxy = await setSecurityQuestionTipModelProxy?.$('.close')
|
||||
|
||||
if (setSecurityQuestionTipModelProxy && closeButtonProxy) {
|
||||
await closeButtonProxy.click()
|
||||
}
|
||||
}
|
||||
|
||||
let cursorToContinueFind = 0
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await pageMapByName.boss?.waitForFunction(() => {
|
||||
return Array.isArray(document.querySelector('.main-wrap .chat-user')?.__vue__?.list)
|
||||
})
|
||||
// find target boss - with unread icon, or recommend system message
|
||||
const friendListData = (await pageMapByName.boss!.evaluate(
|
||||
`
|
||||
document.querySelector('.main-wrap .chat-user')?.__vue__?.list
|
||||
`
|
||||
)) as Array<ChatListItem>
|
||||
const toCheckItemAtIndex = friendListData.findIndex((it, index) => {
|
||||
return (
|
||||
index >= cursorToContinueFind &&
|
||||
(rechatLimitDay && it.updateTime
|
||||
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
|
||||
: true) &&
|
||||
((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) ||
|
||||
canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) &&
|
||||
!it.unreadCount
|
||||
)
|
||||
})
|
||||
|
||||
if (toCheckItemAtIndex < 0) {
|
||||
const isFinished = await pageMapByName.boss!.evaluate(
|
||||
`(document.querySelector(
|
||||
'.main-wrap .chat-user .user-list-content div[role=tfoot] .finished'
|
||||
)?.textContent ?? '').includes('没有')`
|
||||
)
|
||||
if (isFinished) {
|
||||
// list has all loaded and no more target job
|
||||
// go back to first job
|
||||
cursorToContinueFind = 0
|
||||
await pageMapByName.boss?.evaluate(() => {
|
||||
;(() => {
|
||||
document
|
||||
.querySelector('.chat-content .user-list .user-list-content')
|
||||
?.__vue__.scrollToIndex(0)
|
||||
})()
|
||||
})
|
||||
await sleep(10000)
|
||||
} else {
|
||||
cursorToContinueFind = friendListData.length - 1
|
||||
await pageMapByName.boss?.evaluate(() => {
|
||||
;(() => {
|
||||
document
|
||||
.querySelector('.chat-content .user-list .user-list-content')
|
||||
?.__vue__.scrollToBottom()
|
||||
})()
|
||||
})
|
||||
await sleep(3000)
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
cursorToContinueFind = toCheckItemAtIndex
|
||||
await pageMapByName.boss?.evaluate((toCheckItemAtIndex) => {
|
||||
;(() => {
|
||||
document
|
||||
.querySelector('.chat-content .user-list .user-list-content')
|
||||
?.__vue__.scrollToIndex(toCheckItemAtIndex)
|
||||
})()
|
||||
}, toCheckItemAtIndex)
|
||||
await sleep(3000)
|
||||
|
||||
const targetElProxy = await (async () => {
|
||||
const jsHandle = (
|
||||
await pageMapByName.boss?.evaluateHandle((encryptJobId) => {
|
||||
const jobLiEls = document.querySelectorAll(
|
||||
'.main-wrap .chat-user .user-list-content ul[role=group] li[role=listitem]'
|
||||
)
|
||||
return [...jobLiEls].find((it) => {
|
||||
return it.__vue__.source.encryptJobId === encryptJobId
|
||||
})
|
||||
}, friendListData[toCheckItemAtIndex].encryptJobId)
|
||||
)?.asElement()
|
||||
return jsHandle
|
||||
})()
|
||||
await targetElProxy?.click()
|
||||
await pageMapByName.boss!.waitForResponse((response) => {
|
||||
if (response.url().startsWith('https://www.zhipin.com/wapi/zpchat/geek/historyMsg')) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
await sleepWithRandomDelay(1500)
|
||||
const conversationInfo = await pageMapByName.boss?.evaluate(
|
||||
`document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$`
|
||||
)
|
||||
|
||||
const historyMessageList =
|
||||
(
|
||||
await pageMapByName.boss?.evaluate(() => {
|
||||
return document.querySelector('.message-content .chat-record')?.__vue__?.list$ ?? []
|
||||
})
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0
|
||||
if (
|
||||
historyMessageList[historyMessageList.length - 1].isSelf &&
|
||||
historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ &&
|
||||
((conversationInfo &&
|
||||
Object.hasOwn(conversationInfo, 'bothTalked') &&
|
||||
!conversationInfo.bothTalked) ||
|
||||
!historyMessageList.filter(
|
||||
(it) => !it.isSelf // not sent by me
|
||||
).length) &&
|
||||
// don't disturb too much
|
||||
Date.now() - lastGeekMessageSendTime >=
|
||||
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
|
||||
) {
|
||||
await sleepWithRandomDelay(3250)
|
||||
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
|
||||
try {
|
||||
const messageListForGpt = historyMessageList
|
||||
.filter((it) => it.bizType !== 101 && it.isSelf)
|
||||
.slice(-recentMessageQuantityForLlm)
|
||||
await sendGptContent(pageMapByName.boss!, messageListForGpt)
|
||||
gtag('rnrr_llm_content_sent')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) {
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
gtag('rnrr_look_forward_reply_emotion_sent', {
|
||||
fallback: true
|
||||
})
|
||||
} else {
|
||||
gtag('rnrr_encounter_error', {
|
||||
error: err
|
||||
})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
gtag('rnrr_look_forward_reply_emotion_sent')
|
||||
}
|
||||
} else {
|
||||
cursorToContinueFind += 1
|
||||
}
|
||||
await sleep(1000)
|
||||
await saveCurrentChatRecord(pageMapByName.boss!)
|
||||
await sleep(3000)
|
||||
}
|
||||
}
|
||||
|
||||
let isParentProcessDisconnect = false
|
||||
process.once('disconnect', () => {
|
||||
isParentProcessDisconnect = true
|
||||
})
|
||||
const rerunInterval = (() => {
|
||||
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
|
||||
if (isNaN(v)) {
|
||||
v = 3000
|
||||
}
|
||||
|
||||
return v
|
||||
})()
|
||||
|
||||
let pipe
|
||||
try {
|
||||
pipe = fs.createWriteStream(null, { fd: 3 })
|
||||
} catch {
|
||||
console.warn('pipe is not available')
|
||||
}
|
||||
export async function runEntry() {
|
||||
process.on('disconnect', () => {
|
||||
app.exit()
|
||||
})
|
||||
app.dock?.hide()
|
||||
|
||||
while (!isParentProcessDisconnect) {
|
||||
try {
|
||||
await mainLoop()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
try {
|
||||
await pageMapByName['boss']?.close()
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
// handle error
|
||||
if (
|
||||
err instanceof Error &&
|
||||
['LOGIN_STATUS_INVALID', 'ACCESS_IS_DENIED', 'ERR_INTERNET_DISCONNECTED'].includes(
|
||||
err.message
|
||||
)
|
||||
) {
|
||||
pipeWriteRegardlessError(
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
type: err.message
|
||||
}) + '\r\n'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
if (err instanceof Error && err.message === 'CANNOT_FIND_A_USABLE_MODEL') {
|
||||
gtag('cannot_find_a_usable_model')
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
message:
|
||||
'未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题',
|
||||
buttons: ['退出']
|
||||
})
|
||||
process.exit(0)
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
pageMapByName['boss'] = null
|
||||
await sleep(rerunInterval)
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
attachListenerForKillSelfOnParentExited()
|
||||
|
||||
process.once('uncaughtException', (error) => {
|
||||
console.error('uncaughtException', error)
|
||||
process.exit(1)
|
||||
})
|
||||
process.once('unhandledRejection', (error) => {
|
||||
console.log('unhandledRejection', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
async function storeStorage(page) {
|
||||
const [cookies, localStorage] = await Promise.all([
|
||||
page.cookies(),
|
||||
page
|
||||
.evaluate(() => {
|
||||
return JSON.stringify(window.localStorage)
|
||||
})
|
||||
.then((res) => JSON.parse(res))
|
||||
])
|
||||
return Promise.all([
|
||||
writeStorageFile('boss-cookies.json', cookies),
|
||||
writeStorageFile('boss-local-storage.json', localStorage)
|
||||
])
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storag
|
||||
|
||||
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
|
||||
const bossChatUiUrl = `https://www.zhipin.com/web/geek/chat`
|
||||
const bossCookies = readStorageFile('boss-cookies.json')
|
||||
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||||
|
||||
export async function bootstrap() {
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
@@ -28,13 +26,18 @@ export async function bootstrap() {
|
||||
|
||||
export async function launchBoss(browser: Browser) {
|
||||
const page = (await browser.pages())[0]
|
||||
const bossCookies = readStorageFile('boss-cookies.json')
|
||||
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||||
//set cookies
|
||||
for (let i = 0; i < bossCookies.length; i++) {
|
||||
await page.setCookie(bossCookies[i])
|
||||
}
|
||||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
|
||||
try {
|
||||
await Promise.all([page.goto(bossChatUiUrl, { timeout: 0 }), page.waitForNavigation()])
|
||||
await Promise.all([
|
||||
page.goto(bossChatUiUrl, { timeout: 0 }),
|
||||
page.waitForNavigation({ timeout: 120 * 1000 })
|
||||
])
|
||||
} catch (error) {
|
||||
if (error?.message?.startsWith('net::ERR_INTERNET_DISCONNECTED')) {
|
||||
throw new Error('ERR_INTERNET_DISCONNECTED')
|
||||
@@ -0,0 +1,771 @@
|
||||
import { bootstrap, launchBoss } from './bootstrap'
|
||||
import { MsgStatus, type ChatListItem } from './types'
|
||||
import { Browser, Page } from 'puppeteer'
|
||||
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
|
||||
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
|
||||
import { app, dialog } from 'electron'
|
||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||
import {
|
||||
getPublicDbFilePath,
|
||||
readConfigFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatMessageRecord'
|
||||
import {
|
||||
saveChatMessageRecord,
|
||||
getJobHireStatusRecord,
|
||||
saveJobHireStatusRecord
|
||||
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
import {
|
||||
writeStorageFile,
|
||||
readStorageFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||
import {
|
||||
AUTO_CHAT_ERROR_EXIT_CODE,
|
||||
RECHAT_CONTENT_SOURCE,
|
||||
RECHAT_LLM_FALLBACK
|
||||
} from '../../../common/enums/auto-start-chat'
|
||||
import gtag from '../../utils/gtag'
|
||||
import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import dayjs from 'dayjs'
|
||||
import cheerio from 'cheerio'
|
||||
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||
// import { pushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS } from '../../utils/screenshot'
|
||||
import { checkShouldExit } from '../../utils/worker'
|
||||
import minimist from 'minimist'
|
||||
import { checkCookieListFormat } from '../../../common/utils/cookie'
|
||||
import { loginWithCookieAssistant } from '../../features/login-with-cookie-assistant'
|
||||
import initPublicIpc from '../../utils/initPublicIpc'
|
||||
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
|
||||
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('收到SIGTERM信号,正在退出')
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
const throttleIntervalMinutes =
|
||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||
const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21
|
||||
const recentMessageQuantityForLlm =
|
||||
readConfigFile('boss.json').autoReminder?.recentMessageQuantityForLlm ?? 8
|
||||
const rechatContentSource =
|
||||
readConfigFile('boss.json').autoReminder?.rechatContentSource ??
|
||||
RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION
|
||||
const rechatLlmFallback =
|
||||
readConfigFile('boss.json').autoReminder?.rechatLlmFallback ??
|
||||
RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||
|
||||
const expectJobTypeRegExpStr = readConfigFile('boss.json').expectJobTypeRegExpStr
|
||||
const onlyRemindBossWithExpectJobType =
|
||||
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())
|
||||
|
||||
export const pageMapByName: {
|
||||
boss?: Page | null
|
||||
} = {}
|
||||
|
||||
// async function periodPushCurrentPageScreenshot () {
|
||||
// try {
|
||||
// if (pageMapByName.boss?.isClosed()) {
|
||||
// return
|
||||
// }
|
||||
// const shouldExit = await checkShouldExit()
|
||||
// if (shouldExit) {
|
||||
// return
|
||||
// }
|
||||
// try {
|
||||
// await pushCurrentPageScreenshot(pageMapByName.boss)
|
||||
// }
|
||||
// catch (err) {
|
||||
// if (err?.message?.includes(`PAGE_CLOSED`)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
|
||||
// }
|
||||
// catch {}
|
||||
// }
|
||||
|
||||
// periodPushCurrentPageScreenshot()
|
||||
|
||||
async function saveCurrentChatRecord(page) {
|
||||
const userInfo = await page.evaluate(
|
||||
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
|
||||
)
|
||||
const bossInfo = await page.evaluate(
|
||||
'document.querySelector(".chat-conversation .chat-record")?.__vue__?.boss'
|
||||
)
|
||||
|
||||
const ds = await dbInitPromise
|
||||
// save boss info
|
||||
const bossInfoRepository = ds.getRepository(BossInfo)
|
||||
let targetBossInfo = await bossInfoRepository.findOneBy({
|
||||
encryptBossId: bossInfo.encryptBossId
|
||||
})
|
||||
if (!targetBossInfo) {
|
||||
targetBossInfo = new BossInfo()
|
||||
Object.assign(targetBossInfo, {
|
||||
encryptBossId: bossInfo.encryptBossId,
|
||||
name: bossInfo.name,
|
||||
title: bossInfo.title,
|
||||
date: new Date()
|
||||
})
|
||||
await bossInfoRepository.save(targetBossInfo)
|
||||
}
|
||||
|
||||
const rawChatRecordList =
|
||||
(
|
||||
await page.evaluate(
|
||||
'document.querySelector(".message-content .chat-record").__vue__.records$'
|
||||
)
|
||||
)?.filter((msg) => ['received', 'sent'].includes(msg.style)) ?? []
|
||||
const chatRecordList = rawChatRecordList.map((it) => {
|
||||
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
|
||||
mappedItem.mid = it.mid
|
||||
mappedItem.encryptFromUserId =
|
||||
it.style === 'sent'
|
||||
? userInfo.encryptUserId
|
||||
: it.style === 'received'
|
||||
? bossInfo.encryptBossId
|
||||
: ''
|
||||
mappedItem.encryptToUserId =
|
||||
it.style === 'sent'
|
||||
? bossInfo.encryptBossId
|
||||
: it.style === 'received'
|
||||
? userInfo.encryptUserId
|
||||
: ''
|
||||
mappedItem.style = it.style
|
||||
mappedItem.type = it.type
|
||||
mappedItem.time = it.time ? new Date(it.time) : null
|
||||
mappedItem.text = it.text
|
||||
if (it.type === 'image') {
|
||||
mappedItem.imageUrl = it.image?.originImage?.url
|
||||
mappedItem.imageHeight = it.image?.originImage?.url?.height
|
||||
mappedItem.imageWidth = it.image?.originImage?.url?.width
|
||||
}
|
||||
|
||||
return mappedItem
|
||||
})
|
||||
|
||||
await saveChatMessageRecord(ds, chatRecordList)
|
||||
}
|
||||
|
||||
async function checkJobIsClosed() {
|
||||
const encryptJobId = await pageMapByName.boss!.evaluate(() => {
|
||||
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$
|
||||
.encryptJobId
|
||||
})
|
||||
if (!encryptJobId) {
|
||||
return false
|
||||
}
|
||||
let isJobClosed = false
|
||||
let record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
|
||||
// not seen before, or last seen more than 6 hours ago
|
||||
// fetch new status
|
||||
if (
|
||||
!record ||
|
||||
(record.hireStatus === JobHireStatus.HIRING &&
|
||||
Date.now() - Number(dayjs(record.lastSeenDate)) > 6 * 60 * 60 * 1000)
|
||||
) {
|
||||
const positionNameElHandle = await pageMapByName.boss!.$(
|
||||
`#main .chat-conversation [ka="geek_chat_job_detail"] .right-content`
|
||||
)
|
||||
if (!positionNameElHandle) {
|
||||
return false
|
||||
}
|
||||
positionNameElHandle.click()
|
||||
try {
|
||||
const targetPage = await waitForPage(
|
||||
pageMapByName.boss!.browser(),
|
||||
async (page) => {
|
||||
const url = page.url()
|
||||
if (
|
||||
url.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
|
||||
(await page.evaluate(
|
||||
() =>
|
||||
!!document.querySelector('#main .job-banner') ||
|
||||
!!document.documentElement.innerText?.includes(`您访问的页面不存在`)
|
||||
))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
{ timeout: 15 * 1000 }
|
||||
)
|
||||
const htmlContent = await targetPage.content()
|
||||
if (htmlContent) {
|
||||
const $ = cheerio.load(htmlContent)
|
||||
const [jobBannerEl] = $('#main .job-banner') ?? []
|
||||
if (!jobBannerEl) {
|
||||
console.log(`access might be blocked`)
|
||||
if (
|
||||
htmlContent.includes(`您访问的页面不存在`) ||
|
||||
location.href === `https://www.zhipin.com/`
|
||||
) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.DELETED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
|
||||
if (jobStatusTextEl) {
|
||||
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
|
||||
if ([`职位已关闭`].includes(jobStatusText)) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.CLOSED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
} else {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
targetPage.close().catch(() => void 0)
|
||||
} catch (err) {
|
||||
console.log(`checkJobIsClosed error: ${err}`)
|
||||
}
|
||||
record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
|
||||
}
|
||||
if (record.hireStatus !== JobHireStatus.HIRING) {
|
||||
isJobClosed = true
|
||||
}
|
||||
return isJobClosed
|
||||
}
|
||||
|
||||
let browser: null | Browser = null
|
||||
const mainLoop = async () => {
|
||||
if (browser) {
|
||||
try {
|
||||
const cp = browser.process()
|
||||
cp?.kill('SIGKILL')
|
||||
} catch {
|
||||
//
|
||||
} finally {
|
||||
browser = null
|
||||
}
|
||||
}
|
||||
let bossCookies = readStorageFile('boss-cookies.json')
|
||||
let cookieCheckResult = checkCookieListFormat(bossCookies)
|
||||
while (!cookieCheckResult) {
|
||||
try {
|
||||
await loginWithCookieAssistant()
|
||||
bossCookies = readStorageFile('boss-cookies.json')
|
||||
cookieCheckResult = checkCookieListFormat(bossCookies)
|
||||
} catch (err) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `登录状态无效`,
|
||||
detail: `请重新登录BOSS直聘`
|
||||
})
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'basic-cookie-check',
|
||||
status: 'rejected'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
throw new Error('LOGIN_STATUS_INVALID')
|
||||
}
|
||||
}
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'basic-cookie-check',
|
||||
status: 'fulfilled'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
const canNotConfirmIfHasReadMsgTemplateList = [
|
||||
'Boss还没查看你的消息',
|
||||
'你与该职位竞争者PK情况',
|
||||
'简历诊断提醒',
|
||||
'附件简历还没准备好',
|
||||
'设置合适的期望薪资范围'
|
||||
].map((it) => new RegExp(it))
|
||||
browser = await bootstrap()
|
||||
await Promise.all([launchBoss(browser)])
|
||||
|
||||
await sleep(1000)
|
||||
pageMapByName.boss!.bringToFront()
|
||||
await sleep(2000)
|
||||
|
||||
const currentPageUrl = pageMapByName.boss!.url() ?? ''
|
||||
// #region
|
||||
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
|
||||
writeStorageFile('boss-cookies.json', [])
|
||||
try {
|
||||
// popup login dialog, then update login status
|
||||
await loginWithCookieAssistant()
|
||||
} catch (err) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `登录状态无效`,
|
||||
detail: `请重新登录BOSS直聘`
|
||||
})
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'login-status-check',
|
||||
status: 'rejected'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
throw new Error('LOGIN_STATUS_INVALID')
|
||||
}
|
||||
throw new Error('THROW_FOR_RETRY')
|
||||
}
|
||||
if (
|
||||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/403.html') ||
|
||||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/error.html')
|
||||
) {
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'login-status-check',
|
||||
status: 'rejected'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
throw new Error('ACCESS_IS_DENIED')
|
||||
}
|
||||
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/safe/verify-slider')) {
|
||||
const validateRes: any = await pageMapByName
|
||||
.boss!.waitForResponse(
|
||||
(response) => {
|
||||
if (
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpAntispam/v2/geetest/validate')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
{
|
||||
timeout: 0
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
return res.json()
|
||||
})
|
||||
if (validateRes.code === 0) {
|
||||
await storeStorage(pageMapByName.boss)
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'login-status-check',
|
||||
status: 'rejected'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
throw new Error('CAPTCHA_PASSED_AND_NEED_RESTART')
|
||||
}
|
||||
}
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'login-status-check',
|
||||
status: 'fulfilled'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
// check set security question tip modal
|
||||
let setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$(
|
||||
'.dialog-wrap.dialog-account-safe'
|
||||
)
|
||||
if (setSecurityQuestionTipModelProxy) {
|
||||
await sleep(1000)
|
||||
setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$(
|
||||
'.dialog-wrap.dialog-account-safe'
|
||||
)
|
||||
const closeButtonProxy = await setSecurityQuestionTipModelProxy?.$('.close')
|
||||
|
||||
if (setSecurityQuestionTipModelProxy && closeButtonProxy) {
|
||||
await closeButtonProxy.click()
|
||||
}
|
||||
}
|
||||
|
||||
let cursorToContinueFind = 0
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await pageMapByName.boss?.waitForFunction(() => {
|
||||
return Array.isArray(document.querySelector('.main-wrap .chat-user')?.__vue__?.list)
|
||||
})
|
||||
// find target boss - with unread icon, or recommend system message
|
||||
const friendListData = (await pageMapByName.boss!.evaluate(
|
||||
`
|
||||
document.querySelector('.main-wrap .chat-user')?.__vue__?.list
|
||||
`
|
||||
)) as Array<ChatListItem>
|
||||
const toCheckItemAtIndex = friendListData.findIndex((it, index) => {
|
||||
return (
|
||||
index >= cursorToContinueFind &&
|
||||
(onlyRemindBossWithoutBlockCompanyName && blockCompanyNameRegExp
|
||||
? !blockCompanyNameRegExp.test(it.brandName)
|
||||
: true) &&
|
||||
(rechatLimitDay && it.updateTime
|
||||
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
|
||||
: true) &&
|
||||
((((it.lastIsSelf &&
|
||||
it.lastMsgStatus === MsgStatus.HAS_READ &&
|
||||
!it.lastText.includes('你撤回了')) ||
|
||||
canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) &&
|
||||
!it.unreadCount) ||
|
||||
(!it.lastIsSelf && it.lastText === '开场问题,期待你的回答'))
|
||||
)
|
||||
})
|
||||
|
||||
if (toCheckItemAtIndex < 0) {
|
||||
const isFinished = await pageMapByName.boss!.evaluate(
|
||||
`(document.querySelector(
|
||||
'.main-wrap .chat-user .user-list-content div[role=tfoot] .finished'
|
||||
)?.textContent ?? '').includes('没有')`
|
||||
)
|
||||
if (isFinished) {
|
||||
// list has all loaded and no more target job
|
||||
// go back to first job
|
||||
cursorToContinueFind = 0
|
||||
await pageMapByName.boss?.evaluate(() => {
|
||||
;(() => {
|
||||
document
|
||||
.querySelector('.chat-content .user-list .user-list-content')
|
||||
?.__vue__.scrollToIndex(0)
|
||||
})()
|
||||
})
|
||||
await sleep(10000)
|
||||
} else {
|
||||
cursorToContinueFind = friendListData.length - 1
|
||||
await pageMapByName.boss?.evaluate(() => {
|
||||
;(() => {
|
||||
document
|
||||
.querySelector('.chat-content .user-list .user-list-content')
|
||||
?.__vue__.scrollToBottom()
|
||||
})()
|
||||
})
|
||||
await sleep(3000)
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
cursorToContinueFind = toCheckItemAtIndex
|
||||
await pageMapByName.boss?.evaluate((toCheckItemAtIndex) => {
|
||||
;(() => {
|
||||
document
|
||||
.querySelector('.chat-content .user-list .user-list-content')
|
||||
?.__vue__.scrollToIndex(toCheckItemAtIndex)
|
||||
})()
|
||||
}, toCheckItemAtIndex)
|
||||
await sleep(3000)
|
||||
|
||||
const targetElProxy = await (async () => {
|
||||
const jsHandle = (
|
||||
await pageMapByName.boss?.evaluateHandle((encryptJobId) => {
|
||||
const jobLiEls = document.querySelectorAll(
|
||||
'.main-wrap .chat-user .user-list-content ul[role=group] li[role=listitem]'
|
||||
)
|
||||
return [...jobLiEls].find((it) => {
|
||||
return it.__vue__.source.encryptJobId === encryptJobId
|
||||
})
|
||||
}, friendListData[toCheckItemAtIndex].encryptJobId)
|
||||
)?.asElement()
|
||||
return jsHandle
|
||||
})()
|
||||
await targetElProxy?.click()
|
||||
await pageMapByName.boss!.waitForResponse((response) => {
|
||||
if (response.url().startsWith('https://www.zhipin.com/wapi/zpchat/geek/historyMsg')) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
await sleepWithRandomDelay(1500)
|
||||
// check if expect job type match
|
||||
let isExpectJobTypeMatch = true
|
||||
if (onlyRemindBossWithExpectJobType) {
|
||||
const selectedFriendInfo = await pageMapByName.boss?.evaluate(
|
||||
`document.querySelector('.chat-conversation')?.__vue__?.selectedFriend$`
|
||||
)
|
||||
if (!selectedFriendInfo) {
|
||||
isExpectJobTypeMatch = false
|
||||
} else {
|
||||
const jobType = selectedFriendInfo?.positionName
|
||||
if (!jobType) {
|
||||
isExpectJobTypeMatch = false
|
||||
} else {
|
||||
const regExp = new RegExp(expectJobTypeRegExpStr)
|
||||
isExpectJobTypeMatch = regExp.test(jobType)
|
||||
}
|
||||
}
|
||||
}
|
||||
const conversationInfo = await pageMapByName.boss?.evaluate(
|
||||
`document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$`
|
||||
)
|
||||
|
||||
const historyMessageList =
|
||||
(
|
||||
await pageMapByName.boss?.evaluate(() => {
|
||||
return document.querySelector('.message-content .chat-record')?.__vue__?.list$ ?? []
|
||||
})
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0
|
||||
const isJobClosed = await checkJobIsClosed()
|
||||
if (
|
||||
!isJobClosed &&
|
||||
isExpectJobTypeMatch &&
|
||||
historyMessageList[historyMessageList.length - 1].isSelf &&
|
||||
historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ &&
|
||||
((conversationInfo &&
|
||||
Object.hasOwn(conversationInfo, 'bothTalked') &&
|
||||
!conversationInfo.bothTalked) ||
|
||||
!historyMessageList.filter(
|
||||
(it) => !it.isSelf // not sent by me
|
||||
).length) &&
|
||||
// don't disturb too much
|
||||
Date.now() - lastGeekMessageSendTime >=
|
||||
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
|
||||
) {
|
||||
await sleepWithRandomDelay(3250)
|
||||
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
|
||||
try {
|
||||
const messageListForGpt = historyMessageList
|
||||
.filter((it) => it.bizType !== 101 && it.isSelf)
|
||||
.slice(-recentMessageQuantityForLlm)
|
||||
await sendGptContent(pageMapByName.boss!, messageListForGpt)
|
||||
gtag('rnrr_llm_content_sent')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) {
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
gtag('rnrr_look_forward_reply_emotion_sent', {
|
||||
fallback: true
|
||||
})
|
||||
} else {
|
||||
gtag('rnrr_encounter_error', {
|
||||
error: err
|
||||
})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
gtag('rnrr_look_forward_reply_emotion_sent')
|
||||
}
|
||||
} else {
|
||||
cursorToContinueFind += 1
|
||||
}
|
||||
await sleep(1000)
|
||||
await saveCurrentChatRecord(pageMapByName.boss!)
|
||||
await sleep(3000)
|
||||
}
|
||||
}
|
||||
|
||||
const rerunInterval = (() => {
|
||||
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
|
||||
if (isNaN(v)) {
|
||||
v = 3000
|
||||
}
|
||||
|
||||
return v
|
||||
})()
|
||||
|
||||
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
|
||||
export async function runEntry() {
|
||||
app.dock?.hide()
|
||||
await app.whenReady()
|
||||
app.on('window-all-closed', (e) => {
|
||||
e.preventDefault()
|
||||
})
|
||||
initPublicIpc()
|
||||
await connectToDaemon()
|
||||
await sendToDaemon(
|
||||
{
|
||||
type: 'ping'
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'worker-launch',
|
||||
status: 'fulfilled'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
if (!puppeteerExecutable) {
|
||||
try {
|
||||
await configWithBrowserAssistant({ autoFind: true })
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
}
|
||||
if (!puppeteerExecutable) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `未找到可用的浏览器`,
|
||||
detail: `请重新运行本程序,按照提示安装、配置浏览器`
|
||||
})
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'puppeteer-executable-check',
|
||||
status: 'rejected'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
throw new Error(`PUPPETEER_IS_NOT_EXECUTABLE`)
|
||||
}
|
||||
sendToDaemon({
|
||||
type: 'worker-to-gui-message',
|
||||
data: {
|
||||
type: 'prerequisite-step-by-step-checkstep-by-step-check',
|
||||
step: {
|
||||
id: 'puppeteer-executable-check',
|
||||
status: 'fulfilled'
|
||||
},
|
||||
runRecordId
|
||||
}
|
||||
})
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
|
||||
while (true) {
|
||||
try {
|
||||
await mainLoop()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
try {
|
||||
await pageMapByName['boss']?.close()
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
const shouldExit = await checkShouldExit()
|
||||
if (shouldExit) {
|
||||
app.exit()
|
||||
return
|
||||
}
|
||||
// handle error
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('LOGIN_STATUS_INVALID')) {
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
|
||||
break
|
||||
}
|
||||
if (err.message.includes('ERR_INTERNET_DISCONNECTED')) {
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ERR_INTERNET_DISCONNECTED)
|
||||
break
|
||||
}
|
||||
if (err.message.includes('ACCESS_IS_DENIED')) {
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED)
|
||||
break
|
||||
}
|
||||
if (
|
||||
err.message.includes(`PUPPETEER_IS_NOT_EXECUTABLE`) ||
|
||||
err.message.includes(`Could not find Chrome`) ||
|
||||
err.message.includes(`no executable was found`)
|
||||
) {
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
|
||||
break
|
||||
}
|
||||
if (err.message === 'CANNOT_FIND_A_USABLE_MODEL') {
|
||||
gtag('cannot_find_a_usable_model')
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
message:
|
||||
'未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题',
|
||||
buttons: ['退出']
|
||||
})
|
||||
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE)
|
||||
break
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
pageMapByName['boss'] = null
|
||||
await sleep(rerunInterval)
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.once('uncaughtException', (error) => {
|
||||
console.error('uncaughtException', error)
|
||||
process.exit(1)
|
||||
})
|
||||
process.once('unhandledRejection', (error) => {
|
||||
console.log('unhandledRejection', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.once('disconnect', () => {
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
async function storeStorage(page) {
|
||||
const [cookies, localStorage] = await Promise.all([
|
||||
page.cookies(),
|
||||
page
|
||||
.evaluate(() => {
|
||||
return JSON.stringify(window.localStorage)
|
||||
})
|
||||
.then((res) => JSON.parse(res))
|
||||
])
|
||||
return Promise.all([
|
||||
writeStorageFile('boss-cookies.json', cookies),
|
||||
writeStorageFile('boss-local-storage.json', localStorage)
|
||||
])
|
||||
}
|
||||
@@ -1,7 +1,25 @@
|
||||
const runMode = process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE
|
||||
import minimist from 'minimist'
|
||||
import { runCommon } from './features/run-common'
|
||||
import { launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'
|
||||
import { app } from 'electron'
|
||||
|
||||
// 捕获未处理的 EPIPE 错误
|
||||
process.on('uncaughtException', (err) => {
|
||||
if (err?.code === 'EPIPE' || err?.code === 'ERR_STREAM_DESTROYED') {
|
||||
return
|
||||
}
|
||||
throw err
|
||||
})
|
||||
|
||||
const isUiDev = process.env.NODE_ENV === 'development'
|
||||
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
|
||||
console.log(commandlineArgs)
|
||||
|
||||
const runMode = commandlineArgs['mode']
|
||||
|
||||
;(async () => {
|
||||
switch (runMode) {
|
||||
// #region internal use
|
||||
case 'geekAutoStartWithBossMain': {
|
||||
const { waitForProcessHandShakeAndRunAutoChat } = await import(
|
||||
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index'
|
||||
@@ -9,18 +27,9 @@ const runMode = process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE
|
||||
waitForProcessHandShakeAndRunAutoChat()
|
||||
break
|
||||
}
|
||||
case 'geekAutoStartWithBossDaemon': {
|
||||
const { runAutoChatWithDaemon } = await import(
|
||||
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/index'
|
||||
)
|
||||
runAutoChatWithDaemon()
|
||||
break
|
||||
}
|
||||
case 'checkAndDownloadDependenciesForInit': {
|
||||
const { checkAndDownloadDependenciesForInit } = await import(
|
||||
'./flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/index'
|
||||
)
|
||||
checkAndDownloadDependenciesForInit()
|
||||
case 'downloadDependenciesForInit': {
|
||||
const { downloadDependenciesForInit } = await import('./flow/DOWNLOAD_DEPENDENCIES/index')
|
||||
downloadDependenciesForInit()
|
||||
break
|
||||
}
|
||||
case 'launchBossZhipinLoginPageWithPreloadExtension': {
|
||||
@@ -35,15 +44,43 @@ const runMode = process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE
|
||||
launchBossSite()
|
||||
break
|
||||
}
|
||||
case 'readNoReplyAutoReminder': {
|
||||
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER/index')
|
||||
case 'readNoReplyAutoReminderMain': {
|
||||
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER_MAIN/index')
|
||||
runEntry()
|
||||
break
|
||||
}
|
||||
case 'launchDaemon': {
|
||||
await import('./flow/LAUNCH_DAEMON')
|
||||
break
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region user entry
|
||||
case 'geekAutoStartWithBoss': {
|
||||
app.dock?.hide()
|
||||
await launchDaemon()
|
||||
const { isAlreadyRunning } = await runCommon({ mode: 'geekAutoStartWithBossMain' })
|
||||
if (isAlreadyRunning) {
|
||||
process.exit(0)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'readNoReplyAutoReminder': {
|
||||
app.dock?.hide()
|
||||
await launchDaemon()
|
||||
const { isAlreadyRunning } = await runCommon({ mode: 'readNoReplyAutoReminderMain' })
|
||||
if (isAlreadyRunning) {
|
||||
process.exit(0)
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
globalThis.GEEKGEEKRUN_PROCESS_ROLE = 'ui'
|
||||
await launchDaemon()
|
||||
const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index')
|
||||
openSettingWindow()
|
||||
break
|
||||
}
|
||||
// #region
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -2,17 +2,40 @@ import gtag from '.'
|
||||
|
||||
export default class GtagPlugin {
|
||||
apply(hooks) {
|
||||
hooks.newChatStartup.tap('GtagPlugin', (_, { chatStartupFrom }) => {
|
||||
gtag('new_chat_startup', { chatStartupFrom })
|
||||
})
|
||||
hooks.jobMarkedAsNotSuit.tap('GtagPlugin', (_, { markFrom }) => {
|
||||
gtag('job_marked_as_not_suit', { markFrom })
|
||||
hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom, jobSource }) => {
|
||||
gtag('new_chat_startup', {
|
||||
chatStartupFrom,
|
||||
encryptJobId: jobData.jobInfo.encryptId,
|
||||
jobSource
|
||||
})
|
||||
})
|
||||
hooks.jobMarkedAsNotSuit.tap(
|
||||
'GtagPlugin',
|
||||
(jobData, { markFrom, markOp, markReason, jobSource }) => {
|
||||
gtag('job_marked_as_not_suit', {
|
||||
markFrom,
|
||||
markOp,
|
||||
markReason,
|
||||
bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc,
|
||||
encryptJobId: jobData.jobInfo.encryptId,
|
||||
jobSource
|
||||
})
|
||||
}
|
||||
)
|
||||
hooks.noPositionFoundForCurrentJob.tap('GtagPlugin', () => {
|
||||
gtag('no_position_found_for_current_job')
|
||||
})
|
||||
hooks.noPositionFoundAfterTraverseAllJob.tap('GtagPlugin', () => {
|
||||
gtag('no_position_found_after_traverse_all_job')
|
||||
})
|
||||
hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => {
|
||||
gtag('encounter_empty_rec_job_list', { pageQuery })
|
||||
})
|
||||
hooks.sageTimeEnter.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
|
||||
gtag('sage_time_enter', { tag, totalEnabledTimes, recordedOpCount })
|
||||
})
|
||||
hooks.sageTimeExit.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
|
||||
gtag('sage_time_exit', { tag, totalEnabledTimes, recordedOpCount })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { BrowserWindow, ipcMain, shell } from 'electron'
|
||||
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
|
||||
import gtag from './gtag'
|
||||
import buildInfo from '../../common/build-info.json'
|
||||
import os from 'node:os'
|
||||
import fs from 'node:fs'
|
||||
import {
|
||||
ensureStorageFileExist,
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
|
||||
export default function initPublicIpc() {
|
||||
ipcMain.on(
|
||||
@@ -56,4 +62,52 @@ export default function initPublicIpc() {
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
ipcMain.handle('read-storage-file', async (ev, payload) => {
|
||||
ensureStorageFileExist()
|
||||
return await readStorageFile(payload.fileName)
|
||||
})
|
||||
|
||||
ipcMain.handle('write-storage-file', async (ev, payload) => {
|
||||
ensureStorageFileExist()
|
||||
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
|
||||
})
|
||||
ipcMain.handle('get-os-platform', () => {
|
||||
return os.platform()
|
||||
})
|
||||
ipcMain.handle('choose-file', (ev, { fileChooserConfig }) => {
|
||||
if (!fileChooserConfig) {
|
||||
fileChooserConfig = {}
|
||||
}
|
||||
const win = BrowserWindow.fromWebContents(ev.sender)
|
||||
if (!win) {
|
||||
return dialog.showOpenDialog(fileChooserConfig)
|
||||
} else {
|
||||
return dialog.showOpenDialog(win, fileChooserConfig)
|
||||
}
|
||||
})
|
||||
ipcMain.handle('check-executable-file', (ev, filePath: string) => {
|
||||
if (!filePath?.trim()) {
|
||||
return {
|
||||
message: '文件名无效'
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return {
|
||||
message: '文件不存在'
|
||||
}
|
||||
}
|
||||
if (!fs.statSync(filePath).isFile()) {
|
||||
const messageSeg = ['文件不是可执行文件']
|
||||
if (os.platform() === 'darwin') {
|
||||
messageSeg.push(
|
||||
'macOS 平台,可执行文件位于“App包.app/Contents/MacOS/ 文件夹下”,而不是“App包.app”文件夹整体'
|
||||
)
|
||||
}
|
||||
return {
|
||||
message: messageSeg.join(';')
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
67
packages/ui/src/main/utils/screenshot.ts
Normal file
67
packages/ui/src/main/utils/screenshot.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
|
||||
import { checkShouldExit } from "./worker"
|
||||
|
||||
export const SCREENSHOT_INTERVAL_MS = 2500
|
||||
|
||||
export async function pushCurrentPageScreenshot (page) {
|
||||
try {
|
||||
if (!page) {
|
||||
return
|
||||
}
|
||||
// 尝试截图当前页面(压缩为 jpeg + base64,避免文件写盘)
|
||||
const screenshotBase64 = await page.screenshot({
|
||||
type: 'jpeg',
|
||||
quality: 60,
|
||||
encoding: 'base64',
|
||||
fullPage: false
|
||||
})
|
||||
const screenshotAt = Date.now()
|
||||
await sendToDaemon({
|
||||
type: 'worker-screenshot',
|
||||
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
|
||||
data: {
|
||||
screenshot: `data:image/jpeg;base64,${screenshotBase64}`,
|
||||
screenshotAt,
|
||||
pageUrl: page.url?.() ?? null
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
if (err?.message?.includes('Session closed')) {
|
||||
throw new Error(`PAGE_CLOSED`)
|
||||
}
|
||||
// 截图失败不应影响主流程
|
||||
console.warn('[pushCurrentPageScreenshot] error', err)
|
||||
}
|
||||
}
|
||||
|
||||
export class PeriodPushCurrentPageScreenshotPlugin {
|
||||
apply(hooks) {
|
||||
hooks.pageGotten.tap(
|
||||
'PeriodPushCurrentPageScreenshotPlugin',
|
||||
(page) => {
|
||||
async function periodPushCurrentPageScreenshot () {
|
||||
try {
|
||||
if (page.isClosed()) {
|
||||
return
|
||||
}
|
||||
const shouldExit = await checkShouldExit()
|
||||
if (shouldExit) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await pushCurrentPageScreenshot(page)
|
||||
}
|
||||
catch (err) {
|
||||
if (err?.message?.includes(`PAGE_CLOSED`)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
periodPushCurrentPageScreenshot()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
14
packages/ui/src/main/utils/worker.ts
Normal file
14
packages/ui/src/main/utils/worker.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
|
||||
|
||||
export async function checkShouldExit () {
|
||||
const shouldExitResponse = await sendToDaemon(
|
||||
{
|
||||
type: 'check-should-exit',
|
||||
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
|
||||
},
|
||||
{
|
||||
needCallback: true
|
||||
}
|
||||
)
|
||||
return shouldExitResponse?.shouldExit
|
||||
}
|
||||
100
packages/ui/src/main/window/browserAssistantWindow.ts
Normal file
100
packages/ui/src/main/window/browserAssistantWindow.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import path from 'path'
|
||||
import { getAnyAvailablePuppeteerExecutable } from '../flow/DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
|
||||
import {
|
||||
getLastUsedAndAvailableBrowser,
|
||||
saveLastUsedAndAvailableBrowserInfo
|
||||
} from '../flow/DOWNLOAD_DEPENDENCIES/utils/browser-history'
|
||||
import { openBrowserDownloadWindow } from '../features/open-browser-download-window'
|
||||
|
||||
export let browserAssistantWindow: BrowserWindow | null = null
|
||||
|
||||
const registerHandleWithWindow = (
|
||||
win: BrowserWindow,
|
||||
...args: Parameters<typeof ipcMain.handle>
|
||||
) => {
|
||||
const [channel, handler] = args
|
||||
ipcMain.handle(channel, handler)
|
||||
win.once('closed', () => ipcMain.removeHandler(channel))
|
||||
}
|
||||
|
||||
export function createBrowserAssistantWindow(
|
||||
opt?: Electron.BrowserWindowConstructorOptions,
|
||||
{ autoFind } = {}
|
||||
): BrowserWindow {
|
||||
// Create the browser window.
|
||||
if (browserAssistantWindow) {
|
||||
browserAssistantWindow!.close()
|
||||
}
|
||||
browserAssistantWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
minWidth: 800,
|
||||
height: 400,
|
||||
resizable: true,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
},
|
||||
...opt
|
||||
})
|
||||
|
||||
browserAssistantWindow.on('ready-to-show', () => {
|
||||
browserAssistantWindow!.show()
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
let routePath = '#/browserAssistant'
|
||||
if (autoFind) {
|
||||
routePath = '#/browserAutoFind'
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
|
||||
browserAssistantWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + routePath)
|
||||
} else {
|
||||
browserAssistantWindow.loadURL(
|
||||
'file://' + path.join(__dirname, '../renderer/index.html') + routePath
|
||||
)
|
||||
}
|
||||
|
||||
browserAssistantWindow!.once('closed', () => {
|
||||
browserAssistantWindow = null
|
||||
})
|
||||
|
||||
registerHandleWithWindow(
|
||||
browserAssistantWindow,
|
||||
'get-any-available-puppeteer-executable',
|
||||
async (_, { ignoreCached, noSave } = {}) => {
|
||||
return await getAnyAvailablePuppeteerExecutable({ ignoreCached, noSave })
|
||||
}
|
||||
)
|
||||
|
||||
registerHandleWithWindow(
|
||||
browserAssistantWindow,
|
||||
'get-last-used-and-available-browser',
|
||||
async () => {
|
||||
return await getLastUsedAndAvailableBrowser()
|
||||
}
|
||||
)
|
||||
|
||||
registerHandleWithWindow(
|
||||
browserAssistantWindow,
|
||||
'save-last-used-and-available-browser-info',
|
||||
async (_, payload) => {
|
||||
return await saveLastUsedAndAvailableBrowserInfo(payload)
|
||||
}
|
||||
)
|
||||
|
||||
registerHandleWithWindow(browserAssistantWindow, 'download-browser-with-downloader', async () => {
|
||||
return await openBrowserDownloadWindow({
|
||||
windowOption: {
|
||||
parent: browserAssistantWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return browserAssistantWindow!
|
||||
}
|
||||
124
packages/ui/src/main/window/browserDownloadProgressWindow.ts
Normal file
124
packages/ui/src/main/window/browserDownloadProgressWindow.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { ChildProcess } from 'child_process'
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import path from 'path'
|
||||
import * as childProcess from 'node:child_process'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
import * as fs from 'node:fs'
|
||||
import { cacheDir } from '../constant'
|
||||
import { EXPECT_CHROMIUM_BUILD_ID } from '../../common/constant'
|
||||
import * as puppeteerManager from '@puppeteer/browsers'
|
||||
|
||||
export let browserDownloadProgressWindow: BrowserWindow | null = null
|
||||
|
||||
const registerHandleWithWindow = (
|
||||
win: BrowserWindow,
|
||||
...args: Parameters<typeof ipcMain.handle>
|
||||
) => {
|
||||
const [channel, handler] = args
|
||||
ipcMain.handle(channel, handler)
|
||||
win.once('closed', () => ipcMain.removeHandler(channel))
|
||||
}
|
||||
|
||||
export function createBrowserDownloadProgressWindow(
|
||||
opt?: Electron.BrowserWindowConstructorOptions
|
||||
): BrowserWindow {
|
||||
// Create the browser window.
|
||||
if (browserDownloadProgressWindow) {
|
||||
browserDownloadProgressWindow!.close()
|
||||
}
|
||||
browserDownloadProgressWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 200,
|
||||
resizable: false,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
},
|
||||
...opt
|
||||
})
|
||||
|
||||
browserDownloadProgressWindow.on('ready-to-show', () => {
|
||||
browserDownloadProgressWindow!.show()
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
|
||||
browserDownloadProgressWindow.loadURL(
|
||||
process.env['ELECTRON_RENDERER_URL'] + '#/browserDownloadProgress'
|
||||
)
|
||||
} else {
|
||||
browserDownloadProgressWindow.loadURL(
|
||||
'file://' + path.join(__dirname, '../renderer/index.html') + '#/browserDownloadProgress'
|
||||
)
|
||||
}
|
||||
|
||||
let subProcessOfCheckAndDownloadDependencies: ChildProcess | null = null
|
||||
registerHandleWithWindow(browserDownloadProgressWindow, 'setup-dependencies', async () => {
|
||||
if (subProcessOfCheckAndDownloadDependencies) {
|
||||
return
|
||||
}
|
||||
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
|
||||
process.argv[0],
|
||||
[process.argv[1], `--mode=downloadDependenciesForInit`],
|
||||
{
|
||||
stdio: [null, null, null, 'pipe', 'ipc']
|
||||
}
|
||||
)
|
||||
return new Promise((resolve, reject) => {
|
||||
subProcessOfCheckAndDownloadDependencies!.stdio[3]!.pipe(JSONStream.parse()).on(
|
||||
'data',
|
||||
(raw) => {
|
||||
const data = raw
|
||||
switch (data.type) {
|
||||
case 'NEED_RESETUP_DEPENDENCIES':
|
||||
case 'PUPPETEER_DOWNLOAD_PROGRESS': {
|
||||
browserDownloadProgressWindow?.webContents.send(data.type, data)
|
||||
break
|
||||
}
|
||||
case 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR': {
|
||||
console.error(data)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
subProcessOfCheckAndDownloadDependencies!.once('exit', (exitCode) => {
|
||||
const executablePath = puppeteerManager.computeExecutablePath({
|
||||
browser: puppeteerManager.Browser.CHROME,
|
||||
cacheDir,
|
||||
buildId: EXPECT_CHROMIUM_BUILD_ID
|
||||
})
|
||||
if (exitCode === 0 && fs.existsSync(executablePath)) {
|
||||
resolve(executablePath)
|
||||
} else {
|
||||
reject('PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR')
|
||||
}
|
||||
subProcessOfCheckAndDownloadDependencies = null
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const killHandler = async () => {
|
||||
try {
|
||||
subProcessOfCheckAndDownloadDependencies?.kill()
|
||||
} catch {
|
||||
//
|
||||
} finally {
|
||||
subProcessOfCheckAndDownloadDependencies = null
|
||||
}
|
||||
}
|
||||
browserDownloadProgressWindow.once('closed', () => {
|
||||
killHandler()
|
||||
})
|
||||
browserDownloadProgressWindow.once('closed', () => {
|
||||
browserDownloadProgressWindow = null
|
||||
})
|
||||
|
||||
return browserDownloadProgressWindow!
|
||||
}
|
||||
132
packages/ui/src/main/window/cookieAssistantWindow.ts
Normal file
132
packages/ui/src/main/window/cookieAssistantWindow.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { ChildProcess } from 'child_process'
|
||||
import { BrowserWindow, dialog, ipcMain } from 'electron'
|
||||
import path from 'path'
|
||||
import * as childProcess from 'node:child_process'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
import { getLastUsedAndAvailableBrowser } from '../flow/DOWNLOAD_DEPENDENCIES/utils/browser-history'
|
||||
import { configWithBrowserAssistant } from '../features/config-with-browser-assistant'
|
||||
|
||||
export let cookieAssistantWindow: BrowserWindow | null = null
|
||||
export function createCookieAssistantWindow(
|
||||
opt?: Electron.BrowserWindowConstructorOptions
|
||||
): BrowserWindow {
|
||||
// Create the browser window.
|
||||
if (cookieAssistantWindow) {
|
||||
cookieAssistantWindow!.show()
|
||||
}
|
||||
cookieAssistantWindow = new BrowserWindow({
|
||||
width: 960,
|
||||
height: 720,
|
||||
resizable: true,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
},
|
||||
...opt
|
||||
})
|
||||
|
||||
cookieAssistantWindow.on('ready-to-show', () => {
|
||||
cookieAssistantWindow!.show()
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
|
||||
cookieAssistantWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/cookieAssistant')
|
||||
} else {
|
||||
cookieAssistantWindow.loadURL(
|
||||
'file://' + path.join(__dirname, '../renderer/index.html') + '#/cookieAssistant'
|
||||
)
|
||||
}
|
||||
|
||||
cookieAssistantWindow!.once('closed', () => {
|
||||
cookieAssistantWindow = null
|
||||
})
|
||||
|
||||
let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null
|
||||
const launchHandler = async (ev) => {
|
||||
try {
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
if (!puppeteerExecutable) {
|
||||
try {
|
||||
const parent = BrowserWindow.fromWebContents(ev.sender) || undefined
|
||||
await configWithBrowserAssistant({
|
||||
autoFind: true,
|
||||
windowOption: {
|
||||
parent,
|
||||
modal: !!parent,
|
||||
show: true
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
|
||||
}
|
||||
if (!puppeteerExecutable) {
|
||||
await dialog.showMessageBox({
|
||||
type: `error`,
|
||||
message: `未找到可用的浏览器`,
|
||||
detail: `请重新运行本程序,按照提示安装、配置浏览器`
|
||||
})
|
||||
return
|
||||
}
|
||||
const subProcessEnv = {
|
||||
...process.env,
|
||||
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
|
||||
}
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
|
||||
process.argv[0],
|
||||
[process.argv[1], `--mode=launchBossZhipinLoginPageWithPreloadExtension`],
|
||||
{
|
||||
env: subProcessEnv,
|
||||
stdio: [null, null, null, 'pipe', 'ipc']
|
||||
}
|
||||
)
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension!.stdio[3]!.pipe(JSONStream.parse()).on(
|
||||
'data',
|
||||
(raw) => {
|
||||
const data = raw
|
||||
switch (data.type) {
|
||||
case 'BOSS_ZHIPIN_COOKIE_COLLECTED': {
|
||||
cookieAssistantWindow?.webContents.send(data.type, data)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension!.once('exit', () => {
|
||||
cookieAssistantWindow?.webContents.send('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED')
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
|
||||
})
|
||||
}
|
||||
ipcMain.on('launch-bosszhipin-login-page-with-preload-extension', launchHandler)
|
||||
|
||||
const killHandler = async () => {
|
||||
try {
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
|
||||
} catch {
|
||||
//
|
||||
} finally {
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
|
||||
}
|
||||
}
|
||||
ipcMain.on('kill-bosszhipin-login-page-with-preload-extension', killHandler)
|
||||
cookieAssistantWindow.on('closed', () => {
|
||||
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
|
||||
ipcMain.off('launch-bosszhipin-login-page-with-preload-extension', launchHandler)
|
||||
ipcMain.off('kill-bosszhipin-login-page-with-preload-extension', killHandler)
|
||||
})
|
||||
|
||||
return cookieAssistantWindow!
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import path from 'path'
|
||||
import { createFirstLaunchNoticeApproveFlag } from '../features/first-launch-notice-window'
|
||||
|
||||
export let firstLaunchNoticeWindow: BrowserWindow | null = null
|
||||
export function createFirstLaunchNoticeWindow(
|
||||
@@ -41,11 +40,3 @@ export function createFirstLaunchNoticeWindow(
|
||||
|
||||
return firstLaunchNoticeWindow!
|
||||
}
|
||||
|
||||
export const initIpc = () => {
|
||||
ipcMain.handle('first-launch-notice-approve', () => {
|
||||
createFirstLaunchNoticeApproveFlag()
|
||||
firstLaunchNoticeWindow?.close()
|
||||
})
|
||||
}
|
||||
initIpc()
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BrowserWindow, shell } from 'electron'
|
||||
import path from 'path'
|
||||
import { openDevTools } from '../commands'
|
||||
import { createFirstLaunchNoticeWindow } from './firstLaunchNoticeWindow'
|
||||
import { isFirstLaunchNoticeApproveFlagExist } from '../features/first-launch-notice-window'
|
||||
import { daemonEE } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||
export let mainWindow: BrowserWindow | null = null
|
||||
|
||||
export function createMainWindow(): BrowserWindow {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1024,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
minWidth: 1280,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux'
|
||||
@@ -26,14 +26,6 @@ export function createMainWindow(): BrowserWindow {
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
mainWindow.on('ready-to-show', async () => {
|
||||
!isFirstLaunchNoticeApproveFlagExist() &&
|
||||
createFirstLaunchNoticeWindow({
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
})
|
||||
})
|
||||
mainWindow.on('ready-to-show', async () => {
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
setTimeout(() => {
|
||||
@@ -57,5 +49,13 @@ export function createMainWindow(): BrowserWindow {
|
||||
mainWindow!.once('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
daemonEE.on('message', (message) => {
|
||||
if (message.type === 'worker-to-gui-message') {
|
||||
mainWindow?.webContents?.send('worker-to-gui-message', message)
|
||||
}
|
||||
})
|
||||
daemonEE.on('error', (err) => {
|
||||
console.log(err)
|
||||
})
|
||||
return mainWindow!
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>GeekBossGo</title>
|
||||
<title>GeekGeekRun 牛人快跑</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -1,7 +1,42 @@
|
||||
<template>
|
||||
<div class="job-combo-filter">
|
||||
<div class="filter-item">
|
||||
<div>薪资待遇</div>
|
||||
<div font-size-12px>城市</div>
|
||||
<div
|
||||
style="
|
||||
align-items: center;
|
||||
background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
background-image: none;
|
||||
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
|
||||
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
|
||||
"
|
||||
pl4px
|
||||
pr4px
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
>
|
||||
<city-chooser
|
||||
v-model="modelValue.cityList"
|
||||
gt-show-scene="any-combine-boss-recommend-filter"
|
||||
>
|
||||
<template #default="{ showDialog }">
|
||||
<div flex justify-between items-center>
|
||||
<div font-size-12px>
|
||||
<template v-if="modelValue.cityList?.length"
|
||||
>已选择<span ml3px mr3px>{{ modelValue.cityList?.length }}</span
|
||||
>个城市</template
|
||||
>
|
||||
<template v-else><i color-gray>未选择城市</i></template>
|
||||
</div>
|
||||
<el-button size="small" @click="showDialog" pl4px pr4px>选择</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</city-chooser>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<div font-size-12px>薪资待遇</div>
|
||||
<el-select
|
||||
v-model="modelValue.salaryList"
|
||||
multiple
|
||||
@@ -10,7 +45,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.salaryList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.salaryList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -18,7 +53,7 @@
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<div>工作经验</div>
|
||||
<div font-size-12px>工作经验</div>
|
||||
<el-select
|
||||
v-model="modelValue.experienceList"
|
||||
multiple
|
||||
@@ -27,7 +62,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.experienceList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.experienceList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -35,7 +70,7 @@
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<div>学历要求</div>
|
||||
<div font-size-12px>学历要求</div>
|
||||
<el-select
|
||||
v-model="modelValue.degreeList"
|
||||
multiple
|
||||
@@ -44,7 +79,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.degreeList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.degreeList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -52,7 +87,7 @@
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<div>公司行业</div>
|
||||
<div font-size-12px>公司行业</div>
|
||||
<el-select
|
||||
v-model="modelValue.industryList"
|
||||
multiple
|
||||
@@ -75,7 +110,7 @@
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<div>公司规模</div>
|
||||
<div font-size-12px>公司规模</div>
|
||||
<el-select
|
||||
v-model="modelValue.scaleList"
|
||||
multiple
|
||||
@@ -84,7 +119,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.scaleList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.scaleList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -97,6 +132,7 @@
|
||||
<script lang="ts" setup>
|
||||
import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-conditions-20241002.json'
|
||||
import industryFilterExemption from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||||
import CityChooser from '@renderer/page/MainLayout/GeekAutoStartChatWithBoss/components/CityChooser.vue'
|
||||
import { PropType } from 'vue'
|
||||
|
||||
defineProps({
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { random } from 'lodash-es'
|
||||
import { random } from 'lodash'
|
||||
|
||||
const rowCount = 4
|
||||
const colCount = 6
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<el-col :span="6"><el-form-item label="公司">{{ jobInfo.companyName }}</el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="职位名称">{{ jobInfo.jobName }}</el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="职位分类">{{ jobInfo.positionName }}</el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="Boss及其身份">{{ jobInfo.bossName }} {{ jobInfo.bossTitle }}</el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="BOSS及其身份">{{ jobInfo.bossName }} {{ jobInfo.bossTitle }}</el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<el-divider content-position="left">变更记录</el-divider>
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<draggable
|
||||
:model-value="modelValue"
|
||||
class="list-group"
|
||||
:component-data="{
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !drag ? 'flip-list' : null
|
||||
}"
|
||||
:group="{ name: 'parent', put: ['parent'] }"
|
||||
v-bind="dragOptions"
|
||||
handle=".drag-handle-wrap"
|
||||
@update:model-value="emit('update:model-value', $event)"
|
||||
@start="drag = true"
|
||||
@end="
|
||||
() => {
|
||||
drag = false
|
||||
setTimeout(() => {
|
||||
gtagRenderer('job-source-dragged', {
|
||||
sourceOrder: modelValue?.map((it) => JobSource[it.type]).join(','),
|
||||
enabledSourceOrder: modelValue
|
||||
?.filter?.((it) => !!it.enabled)
|
||||
?.map((it) => JobSource[it.type])
|
||||
?.join(',')
|
||||
})
|
||||
}, 50)
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<li class="list-group-item">
|
||||
<div flex flex-items-center>
|
||||
<span class="drag-handle-wrap">
|
||||
<span class="drag-handle" />
|
||||
</span>
|
||||
<span class="inner-content">
|
||||
<div flex w-full flex-1>
|
||||
<template
|
||||
v-if="
|
||||
element.type === 'search' &&
|
||||
(!element?.children?.length ||
|
||||
element?.children?.every((it) => !(it.enabled && !!it.keyword?.trim())))
|
||||
"
|
||||
>
|
||||
<el-switch
|
||||
model-value="false"
|
||||
mr10px
|
||||
disabled
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@click="
|
||||
() => {
|
||||
if (!element?.children?.length) {
|
||||
Message.info({
|
||||
message: '请添加一个要搜索的关键词',
|
||||
grouping: true
|
||||
})
|
||||
} else if (
|
||||
element?.children?.every((it) => !(it.enabled && !!it.keyword?.trim()))
|
||||
) {
|
||||
Message.info({
|
||||
message: '请启用一个要搜索的关键词',
|
||||
grouping: true
|
||||
})
|
||||
}
|
||||
gtagRenderer('job-source-switch-ph-clicked', { type: element.type })
|
||||
}
|
||||
"
|
||||
/>
|
||||
{{ element.label }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-switch
|
||||
v-model="element.enabled"
|
||||
mr10px
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@change="
|
||||
(v) => gtagRenderer('job-source-switch-changed', { type: element.type, v })
|
||||
"
|
||||
/>
|
||||
{{ element.label }}
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="element.type === 'search'">
|
||||
<div flex flex-items-center>
|
||||
<span color-orange align-self-end mr10px>
|
||||
{{ getSearchSourceTipText(element) }}
|
||||
</span>
|
||||
<el-button
|
||||
p-0
|
||||
h-fit
|
||||
type="text"
|
||||
@click="
|
||||
() => {
|
||||
addSearchKeyword(element)
|
||||
gtagRenderer('job-source-search-kw-added', {
|
||||
kwListLength: element.children?.length
|
||||
})
|
||||
}
|
||||
"
|
||||
>添加关键词</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="element.type === 'search' && element?.children?.length">
|
||||
<draggable
|
||||
v-model="element.children"
|
||||
class="list-group"
|
||||
:component-data="{
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !drag ? 'flip-list' : null
|
||||
}"
|
||||
:group="{ name: 'child', put: ['child'] }"
|
||||
v-bind="dragOptions"
|
||||
handle=".drag-handle-wrap"
|
||||
@start="drag = true"
|
||||
@end="
|
||||
() => {
|
||||
drag = false
|
||||
setTimeout(() => {
|
||||
gtagRenderer('job-source-search-kw-dragged', {
|
||||
kwListLength: element.children?.length
|
||||
})
|
||||
}, 50)
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #item="{ element: searchItem, index }">
|
||||
<li class="list-group-item">
|
||||
<div flex flex-items-center>
|
||||
<span class="drag-handle-wrap">
|
||||
<span class="drag-handle" />
|
||||
</span>
|
||||
<span class="inner-content">
|
||||
<div flex w-full>
|
||||
<el-switch
|
||||
v-if="element.enabled && searchItem.keyword?.trim()"
|
||||
v-model="searchItem.enabled"
|
||||
mr10px
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@change="(v) => gtagRenderer('job-source-search-kw-switch-changed', { v })"
|
||||
/>
|
||||
<el-switch
|
||||
v-else
|
||||
disabled
|
||||
:model-value="false"
|
||||
mr10px
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@click="
|
||||
() => {
|
||||
if (!searchItem.keyword?.trim()) {
|
||||
Message.info({
|
||||
message: '该条关键词为空,请输入要搜索的关键词',
|
||||
grouping: true
|
||||
})
|
||||
} else if (!element.enabled) {
|
||||
Message.info({
|
||||
message: '请启用“通过搜索找到的职位”来源',
|
||||
grouping: true
|
||||
})
|
||||
}
|
||||
gtagRenderer('job-source-search-kw-switch-ph-clicked')
|
||||
}
|
||||
"
|
||||
/>
|
||||
<el-input
|
||||
v-model="searchItem.keyword"
|
||||
maxlength="100"
|
||||
@blur="
|
||||
() => {
|
||||
searchItem.keyword = searchItem.keyword?.trim() ?? ''
|
||||
gtagRenderer('job-source-search-kw-input-blurred', {
|
||||
contentLength: searchItem.keyword?.length
|
||||
})
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
p-0
|
||||
ml-10px
|
||||
h-fit
|
||||
type="danger"
|
||||
link
|
||||
@click="
|
||||
() => {
|
||||
removeSearchKeywordByIndex(element, index)
|
||||
gtagRenderer('job-source-search-kw-removed', {
|
||||
itemIndex: index,
|
||||
contentLength: searchItem.keyword?.length
|
||||
})
|
||||
}
|
||||
"
|
||||
>删除</el-button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { ElMessage as Message } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { JobSource } from '@geekgeekrun/sqlite-plugin/src/enums'
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array
|
||||
}
|
||||
})
|
||||
|
||||
const setTimeout = globalThis.setTimeout.bind(globalThis)
|
||||
|
||||
const emit = defineEmits(['update:model-value'])
|
||||
|
||||
const drag = ref(false)
|
||||
|
||||
const dragOptions = computed(() => {
|
||||
return {
|
||||
animation: 200,
|
||||
disabled: false,
|
||||
ghostClass: 'ghost'
|
||||
}
|
||||
})
|
||||
|
||||
function addSearchKeyword(item) {
|
||||
if (!item.children) {
|
||||
item.children = []
|
||||
}
|
||||
item.enabled = true
|
||||
item.children.push({
|
||||
type: 'search-kw',
|
||||
enabled: true,
|
||||
keyword: ''
|
||||
})
|
||||
}
|
||||
|
||||
function removeSearchKeywordByIndex(item, index) {
|
||||
item.children?.splice(index, 1)
|
||||
}
|
||||
const searchKeywordCountMap = computed(() => {
|
||||
const target = props.modelValue?.find((it) => it.type === 'search' && it.enabled)
|
||||
if (!target || !target?.children?.length) {
|
||||
return {}
|
||||
}
|
||||
const map = {}
|
||||
target.children?.forEach((it) => {
|
||||
if (!it.keyword) {
|
||||
return
|
||||
}
|
||||
if (!map[it.keyword]) {
|
||||
map[it.keyword] = 0
|
||||
}
|
||||
map[it.keyword]++
|
||||
})
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
const getSearchSourceTipText = (element) => {
|
||||
if (!element?.children?.length) {
|
||||
return `添加一个关键词后方可启用->`
|
||||
}
|
||||
const seg: string[] = []
|
||||
if (element.enabled) {
|
||||
if (element.children?.every((it) => !(it.enabled && !!it.keyword?.trim()))) {
|
||||
seg.push(`启用下方任一非空项后方可启用`)
|
||||
} else if (element.children?.some((it) => it.enabled && !it.keyword?.trim())) {
|
||||
seg.push(`空项会被跳过`)
|
||||
}
|
||||
if (Object.values(searchKeywordCountMap.value).some((n) => n > 1)) {
|
||||
seg.push(`重复项仅取第一个启用的项`)
|
||||
}
|
||||
}
|
||||
return seg.join(';')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.list-group {
|
||||
line-height: 32px;
|
||||
min-height: 20px;
|
||||
.list-group {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.list-group-item {
|
||||
background-color: #fff;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-right: 10px;
|
||||
border: 1px solid var(--el-card-border-color);
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
border-radius: 4px;
|
||||
.inner-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
::v-deep(.el-checkbox) {
|
||||
.el-checkbox__label {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.drag-handle-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 25px;
|
||||
height: 1em;
|
||||
cursor: grab;
|
||||
margin-right: 4px;
|
||||
.drag-handle {
|
||||
width: 20px;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 35%,
|
||||
var(--el-card-border-color) 35%,
|
||||
var(--el-card-border-color) 45%,
|
||||
transparent 45%,
|
||||
transparent 55%,
|
||||
var(--el-card-border-color) 55%,
|
||||
var(--el-card-border-color) 65%,
|
||||
transparent 65%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
166
packages/ui/src/renderer/src/features/RunningOverlay/index.vue
Normal file
166
packages/ui/src/renderer/src/features/RunningOverlay/index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
modal-class="runing-overlay__modal"
|
||||
:model-value="isDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
>
|
||||
<!-- v-if="stepsForRender.some(it => ['todo', 'pending', 'rejected'].includes(it.status))" -->
|
||||
<div>
|
||||
<ul m0 pl0>
|
||||
<li
|
||||
v-for="(item, index) in stepsForRender"
|
||||
:key="index"
|
||||
list-style-none
|
||||
flex
|
||||
justify-start
|
||||
pt4px
|
||||
pb4px
|
||||
>
|
||||
<div>
|
||||
<span v-if="item.status === 'todo'">🕐</span>
|
||||
<span v-if="item.status === 'pending'">👉</span>
|
||||
<span v-if="item.status === 'fulfilled'">✅</span>
|
||||
<span v-if="item.status === 'rejected'">⛔️</span>
|
||||
</div>
|
||||
<span ml8px>{{ item.describe }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div flex justify-between items-center w-full>
|
||||
<div>
|
||||
{{ runningStatusTextMapByCode[currentRunningStatus] }}
|
||||
</div>
|
||||
<div>
|
||||
<slot name="op-buttons" :current-running-status="currentRunningStatus" />
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// import { useTaskManagerStore } from '@renderer/store'
|
||||
import { getAutoStartChatSteps } from '../../../../common/prerequisite-step-by-step-check'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import {
|
||||
AUTO_CHAT_ERROR_EXIT_CODE,
|
||||
RUNNING_STATUS_ENUM
|
||||
} from '../../../../common/enums/auto-start-chat'
|
||||
const props = defineProps({
|
||||
workerId: {
|
||||
type: String
|
||||
},
|
||||
runRecordId: {
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
// const taskManagerStore = useTaskManagerStore()
|
||||
// const runingTaskInfo = computed(() => {
|
||||
// return taskManagerStore.runningTasks?.find((it) => {
|
||||
// return it.workerId === props.workerId
|
||||
// })
|
||||
// })
|
||||
const steps = ref([])
|
||||
const stepsForRender = computed(() => {
|
||||
const clonedSteps = JSON.parse(JSON.stringify(steps.value))
|
||||
if (clonedSteps.some((it) => it.status === 'rejected')) {
|
||||
return clonedSteps
|
||||
}
|
||||
const lastFulfilledIndex = clonedSteps.findLastIndex((it) => it.status === 'fulfilled')
|
||||
if (lastFulfilledIndex + 1 < clonedSteps.length) {
|
||||
clonedSteps[lastFulfilledIndex + 1].status = 'pending'
|
||||
}
|
||||
return clonedSteps
|
||||
})
|
||||
const runningStatusTextMapByCode = {
|
||||
[RUNNING_STATUS_ENUM.RUNNING]: '正在运行中',
|
||||
[RUNNING_STATUS_ENUM.NORMAL_EXITED]: '程序已正常退出',
|
||||
[RUNNING_STATUS_ENUM.ERROR_EXITED]: '程序异常退出'
|
||||
}
|
||||
const currentRunningStatus = ref(RUNNING_STATUS_ENUM.RUNNING)
|
||||
function fillEmptySteps() {
|
||||
const arr = getAutoStartChatSteps()
|
||||
arr.forEach((it) => (it.status = 'todo'))
|
||||
steps.value = arr
|
||||
currentRunningStatus.value = RUNNING_STATUS_ENUM.RUNNING
|
||||
}
|
||||
watch(() => props.runRecordId, fillEmptySteps, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const { ipcRenderer } = electron
|
||||
function messageHandler(ev, { data }) {
|
||||
if (
|
||||
data.type !== 'prerequisite-step-by-step-checkstep-by-step-check' ||
|
||||
data.runRecordId !== props.runRecordId
|
||||
) {
|
||||
return
|
||||
}
|
||||
const { id: stepId, status: stepStatus } = data.step
|
||||
const targetStep = steps.value.find((it) => it.id === stepId)
|
||||
if (!targetStep) {
|
||||
return
|
||||
}
|
||||
targetStep.status = stepStatus
|
||||
}
|
||||
const unListenMessage = ipcRenderer.on('worker-to-gui-message', messageHandler)
|
||||
onUnmounted(unListenMessage)
|
||||
|
||||
const isDialogVisible = ref(false)
|
||||
const show = () => {
|
||||
isDialogVisible.value = true
|
||||
}
|
||||
const hide = () => {
|
||||
isDialogVisible.value = false
|
||||
}
|
||||
defineExpose({
|
||||
show,
|
||||
hide
|
||||
})
|
||||
ipcRenderer.on('worker-exited', (ev, payload) => {
|
||||
const { workerId, code } = payload
|
||||
if (
|
||||
workerId !== props.workerId
|
||||
// || runRecordId !== props.runRecordId
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (code !== AUTO_CHAT_ERROR_EXIT_CODE.NORMAL) {
|
||||
currentRunningStatus.value = RUNNING_STATUS_ENUM.ERROR_EXITED
|
||||
} else {
|
||||
currentRunningStatus.value = RUNNING_STATUS_ENUM.NORMAL_EXITED
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-overlay.runing-overlay__modal {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(3px);
|
||||
|
||||
background-color: transparent;
|
||||
background-image: radial-gradient(transparent 1px, #fff 1px);
|
||||
background-size: 4px 4px;
|
||||
|
||||
.el-overlay-dialog {
|
||||
position: absolute;
|
||||
pointer-events: all;
|
||||
}
|
||||
.el-dialog {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
.el-dialog__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button p-0 h-auto flex size="small" type="text" :icon="Plus" @click="addCondition"
|
||||
>添加条件</el-button
|
||||
>
|
||||
<div class="job-combo-filter" mt-8px>
|
||||
<el-table
|
||||
size="small"
|
||||
:data="props.modelValue"
|
||||
border
|
||||
:style="{ maxWidth: '100%' }"
|
||||
:row-style="
|
||||
({ row }) => {
|
||||
return {
|
||||
backgroundColor:
|
||||
duplicatedMap.get(getStaticCombineFilterKey(row))?.length > 1
|
||||
? '#fcd4b7'
|
||||
: 'transparent'
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #empty>
|
||||
<div lh-1.5em>
|
||||
列表中没有条件,将仅使用默认的“初始空条件”为您筛选职位<br />
|
||||
你可以点击表格左上角“<el-button
|
||||
p-0
|
||||
h-auto
|
||||
size="small"
|
||||
type="text"
|
||||
:icon="Plus"
|
||||
@click="addCondition"
|
||||
>添加条件</el-button
|
||||
>”按钮,添加更多筛选条件。
|
||||
</div>
|
||||
</template>
|
||||
<el-table-column :resizable="false" label="" :width="80">
|
||||
<template #default="{ $index: index }">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '2px',
|
||||
height: 'fit-content'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveConditionUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= modelValue?.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveConditionDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="城市" prop="city">
|
||||
<template #default="{ row }">
|
||||
<city-chooser
|
||||
v-model="row.city"
|
||||
:multiple="false"
|
||||
gt-show-scene="static-combine-boss-recommend-filter"
|
||||
>
|
||||
<template #default="{ showDialog }">
|
||||
<div flex justify-between items-center>
|
||||
<div font-size-12px lh-1.2em>
|
||||
<template v-if="row.city">{{ row.city }}</template>
|
||||
<template v-else><i color-gray>未选择城市</i></template>
|
||||
</div>
|
||||
<el-button size="small" pl4px pr4px @click="showDialog">选择</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</city-chooser>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="薪资待遇" prop="salary">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.salary"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.salaryList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="工作经验" prop="experience">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.experience"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.experienceList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="学历要求" prop="degree">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.degree"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.degreeList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="公司行业" :width="200" prop="industry">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.industry"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option-group
|
||||
v-for="group in industryFilterExemption"
|
||||
:key="group.code"
|
||||
:label="group.name"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in group.subLevelModelList"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
/>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="公司规模" prop="scale">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.scale"
|
||||
:disabled="row.___itemType === 'empty-condition-placeholder'"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.scaleList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
Array.from(duplicatedMap.values()).some((it) => {
|
||||
return it.length > 1
|
||||
})
|
||||
"
|
||||
color-orange
|
||||
font-size-12px
|
||||
>
|
||||
列表中被橙色高亮的条件存在重复项,相关重复项将被合并,运行时遍历顺序以第一次出现为准
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-conditions-20241002.json'
|
||||
import industryFilterExemption from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||||
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { computed, PropType } from 'vue'
|
||||
import CityChooser from '@renderer/page/MainLayout/GeekAutoStartChatWithBoss/components/CityChooser.vue'
|
||||
|
||||
import { getStaticCombineFilterKey } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<
|
||||
Array<{
|
||||
salary: number | null
|
||||
experience: number | null
|
||||
degree: number | null
|
||||
industry: number | null
|
||||
scale: number | null
|
||||
}>
|
||||
>,
|
||||
default: () => []
|
||||
},
|
||||
isSkipEmptyConditionForCombineRecommendJobFilter: {
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
// fix for misspell of scale property.
|
||||
for (const condition of props.modelValue) {
|
||||
if ((condition as any).scaleList && !condition.scale) {
|
||||
condition.scale = (condition as any).scaleList
|
||||
delete (condition as any).scaleList
|
||||
}
|
||||
}
|
||||
|
||||
function getNewConditionItem() {
|
||||
return {
|
||||
salary: null,
|
||||
experience: null,
|
||||
degree: null,
|
||||
industry: null,
|
||||
scale: null
|
||||
}
|
||||
}
|
||||
|
||||
function addCondition() {
|
||||
props.modelValue?.push(getNewConditionItem())
|
||||
// gtagRenderer('resume_work_exp_added')
|
||||
}
|
||||
function moveConditionUp(index) {
|
||||
;[props.modelValue[index], props.modelValue[index - 1]] = [
|
||||
props.modelValue[index - 1],
|
||||
props.modelValue[index]
|
||||
]
|
||||
// gtagRenderer('resume_work_exp_moved_up')
|
||||
}
|
||||
|
||||
function moveConditionDown(index) {
|
||||
;[props.modelValue[index], props.modelValue[index + 1]] = [
|
||||
props.modelValue[index + 1],
|
||||
props.modelValue[index]
|
||||
]
|
||||
// gtagRenderer('resume_work_exp_moved_down')
|
||||
}
|
||||
|
||||
function removeCondition(index) {
|
||||
props.modelValue?.splice(index, 1)
|
||||
// gtagRenderer('resume_work_exp_removed')
|
||||
}
|
||||
const duplicatedMap = computed(() => {
|
||||
const map = new Map()
|
||||
for (const condition of props.modelValue ?? []) {
|
||||
const key = getStaticCombineFilterKey(condition)
|
||||
if (!map.has(key)) {
|
||||
map.set(key, [])
|
||||
}
|
||||
const arr = map.get(key)
|
||||
arr.push(condition)
|
||||
}
|
||||
return map
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.job-combo-filter {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user