Compare commits

...

92 Commits

Author SHA1 Message Date
geekgeekrun
418ca66a30 ui-v0.12.0 2025-12-24 23:43:32 +08:00
geekgeekrun
0928b402fb Merge remote-tracking branch 'origin/master' into feature/ui 2025-12-24 21:48:36 +08:00
geekgeekrun-maintainer
373e45dee8 update readme 2025-12-24 16:35:17 +08:00
geekgeekrun
8bbfa5b41e add job status check for ghosting reminder 2025-12-24 15:44:15 +08:00
geekgeekrun
aa410cfc60 add job hire status record table and relevant save logic 2025-12-23 17:05:21 +08:00
geekgeekrun
e986fddbfb do not remind chat manual revoked by user 2025-12-23 13:24:52 +08:00
geekgeekrun
cf7a0d3902 Merge branch 'feature/ui' 2025-12-17 14:32:56 +08:00
geekgeekrun
c95349c037 Merge remote-tracking branch 'origin/master' into feature/ui 2025-12-16 23:07:02 +08:00
geekgeekrun-maintainer
8197782102 Replace old image with new one in README
Updated image in README.md to a new source.
2025-12-15 22:59:44 +08:00
geekgeekrun
f17d626ea0 ui-v0.11.0 2025-12-15 18:02:41 +08:00
geekgeekrun
2b4b620977 fix match rule for multi line in testIfJobTitleOrDescriptionSuit 2025-12-15 18:01:52 +08:00
geekgeekrun
03e3c3661f adjust default config of sage time 2025-12-14 20:59:50 +08:00
geekgeekrun
5d787fdd50 add sage time intro 2025-12-14 13:22:56 +08:00
geekgeekrun
b68ff63b6b add sage time gtag in renderer process 2025-12-14 12:08:11 +08:00
geekgeekrun
2aee76307f add sage time gtag in main process 2025-12-14 11:15:22 +08:00
geekgeekrun
e2040caa38 add sage time ui, make it configurable 2025-12-14 10:46:19 +08:00
geekgeekrun
fba45e420f add sage time for pausing when request count to a time 2025-12-13 15:15:09 +08:00
geekgeekrun
fe629eaa22 ui-v0.10.2 2025-12-12 22:10:32 +08:00
geekgeekrun
056ec7a0a6 handle dialog about /猎头/ during startup chat 2025-12-12 22:03:56 +08:00
geekgeekrun
f27ca4145a handle dialog about /剩\d+次沟通机会/ during startup chat 2025-12-12 22:02:57 +08:00
geekgeekrun
cd09210017 add the tip dialog of LOGIN_STATUS_INVALID 2025-12-12 20:14:04 +08:00
geekgeekrun
aec72c28f0 ui-v0.10.1 2025-12-10 14:44:21 +08:00
geekgeekrun
69cf5fd34f upgrade macOS version in build ci workflow file 2025-12-10 14:44:01 +08:00
geekgeekrun
5f47e47448 upgrade macOS version in build ci workflow file 2025-12-10 13:26:29 +08:00
geekgeekrun
4e9b44d04c fix for misspell of scale property. 2025-12-10 13:16:15 +08:00
geekgeekrun
d206d45242 fix when return value of formatStaticCombineFilters is a empty array, program cannot run correctly 2025-12-10 13:15:55 +08:00
geekgeekrun
0aef2af350 ui-v0.10.0 2025-11-01 23:45:16 +08:00
geekgeekrun
c3dc75b503 ui-v0.9.5 2025-11-01 23:15:13 +08:00
geekgeekrun
7a1ae48aa7 Merge branch 'feature/static-combine-job-filter' into feature/ui 2025-11-01 23:14:47 +08:00
geekgeekrun
e5225006cc form validate stage change 2025-11-01 23:13:24 +08:00
geekgeekrun
fdf2c61620 support multi line regexp 2025-11-01 00:39:44 +08:00
geekgeekrun
1f140121fb handle duplicate static combine conditions 2025-10-26 19:12:13 +08:00
geekgeekrun
57cf631b86 add static combine job filter 2025-10-26 18:00:35 +08:00
geekgeekrun
36aef1c16d add more position presets 2025-10-24 01:42:30 +08:00
geekgeekrun
d21972020e ui-v0.9.4 2025-10-14 01:52:07 +08:00
geekgeekrun
51ca5dd71d fix when choose "应届生" in "工作经验", every job is marked as not suit 2025-10-14 01:49:57 +08:00
geekgeekrun
e7d5ef382e ui-v0.9.3 2025-10-13 00:44:31 +08:00
geekgeekrun
59a36dc7bd Merge remote-tracking branch 'origin/master' into feature/ui 2025-10-13 00:43:05 +08:00
geekgeekrun
73b57e21e4 fix tip of daily chat startup chance used out. fix noOpCondition is not take effect. 2025-10-13 00:42:48 +08:00
geekgeekrun
6f0d98653c enhance ui text 2025-10-13 00:39:31 +08:00
geekgeekrun
3bf6d4c358 enhance electron.vite.config.ts 2025-10-04 01:24:18 +08:00
geekgeekrun
2c7ecafee2 fix issue will cause app crash when mark job not suit due to salary; fix salary compare logic 2025-10-04 01:23:01 +08:00
geekgeekrun
f88a5a2889 Merge branch 'feature/ui' 2025-08-17 13:51:09 +08:00
geekgeekrun
ac19ae3832 ui-v0.9.2 2025-08-17 11:16:42 +08:00
geekgeekrun
a25b047f99 fix ci error  ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with packages/ui/package.json 2025-08-17 11:16:14 +08:00
geekgeekrun
42fc66a0f4 fix cannot run due to lodash-es is not bundled; fix logic about set security question tip modal 2025-08-17 11:11:15 +08:00
geekgeekrun
203fbba9a0 ui-v0.9.1 2025-08-17 01:53:49 +08:00
geekgeekrun
b821bc500c fix error for cannot find chooseReasonDialogProxy 2025-08-17 01:50:51 +08:00
geekgeekrun
0032a356e5 fix: reduce argument getBossIdsByJobIds count to fix error: Error: SQLITE_ERROR: Expression tree is too large (maximum depth 1000) 2025-08-17 00:04:05 +08:00
geekgeekrun
b9c4976291 fix: cannot run due to QueryFailedError: SQLITE_ERROR: near ")": syntax error - when one of jobNotMatchStrategy/jobNotActiveStrategy/expectCityNotMatchStrategy set to MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL and chattedJobIds.length === 0, application will crash 2025-08-17 00:00:34 +08:00
geekgeekrun
558891ad9f Merge branch 'master' into feature/ui 2025-08-16 16:38:34 +08:00
geekgeekrun-maintainer
d2da9f5a7d Update README.md 2025-07-31 01:18:29 +08:00
geekgeekrun-maintainer
71068be21f Update README.md 2025-07-25 02:27:45 +08:00
geekgeekrun-maintainer
2b0cf2a2fa Update README.md 2025-07-15 13:44:41 +08:00
geekgeekrun-maintainer
ceaf68e4eb Update README.md 2025-07-15 13:28:39 +08:00
geekgeekrun-maintainer
10cc248784 Update README.md 2025-07-15 13:23:04 +08:00
geekgeekrun
1d155e498a ui-v0.9.0 2025-07-13 21:12:01 +08:00
geekgeekrun
d3e086f321 Merge branch 'feature/ui' 2025-07-13 21:10:34 +08:00
geekgeekrun
5f97b3ccec Merge remote-tracking branch 'origin/master' into feature/ui 2025-07-13 21:10:07 +08:00
geekgeekrun
7dd0c675d5 enhance agreement 2025-07-13 21:09:40 +08:00
geekgeekrun-maintainer
c1978417da Update README.md 2025-07-13 20:16:12 +08:00
geekgeekrun
e0670a9f30 enhance gtag 2025-07-13 19:53:25 +08:00
geekgeekrun
b05031be8a adjust job-source-dragged gtag value 2025-07-13 18:48:05 +08:00
geekgeekrun
7b10e97fa8 collect jobSource when manual startup chat running; add jobSource in gtag both auto or manual 2025-07-13 18:40:59 +08:00
geekgeekrun
e867acb39d collect jobSource when auto startup chat running 2025-07-13 14:27:43 +08:00
geekgeekrun
b31224a81c add job source column migration for db 2025-07-13 14:19:23 +08:00
geekgeekrun
ddb0cb97d7 add gtag 2025-07-13 11:32:54 +08:00
geekgeekrun
f338dcffc8 enhance ui text for SearchSourceTipText 2025-07-13 01:32:36 +08:00
geekgeekrun
84a79c9bae enhance job-detail-filter error style 2025-07-13 00:55:43 +08:00
geekgeekrun
44b0e44e2e enhance ux of job source 2025-07-13 00:34:57 +08:00
geekgeekrun
08028f37dd add fallback logic when find no JobSource enabled 2025-07-12 20:04:32 +08:00
geekgeekrun
a0b09ebae4 Merge remote-tracking branch 'origin/master' into feature/ui 2025-07-12 19:32:50 +08:00
geekgeekrun
f078500c14 add core logic for job source 2025-07-12 19:31:14 +08:00
geekgeekrun
b89d6e264d fix wrong call way for mainFlowWillLaunch hook 2025-07-12 18:50:40 +08:00
geekgeekrun
78ddc949f4 enhance ui text 2025-07-12 13:01:48 +08:00
geekgeekrun
a208ffe7d4 fix TypeError: notSuitConditionHandleMap[markOnBossCondition] is not a function 2025-07-08 10:07:18 +08:00
geekgeekrun
bc70031e2b add desc of position filter 2025-07-06 04:33:34 +08:00
geekgeekrun
cc821f46f5 add logic to skip chatted boss in 30 days for auto-startup-chat 2025-07-06 02:51:12 +08:00
geekgeekrun
f668b3e5f5 add the logic to mark unsuitable job on local 2025-07-06 02:00:53 +08:00
geekgeekrun
d1441b5761 WIP: add the feature to apply job from recommend list and search list - TODO: handle mark as not suit 2025-07-05 20:09:17 +08:00
geekgeekrun-maintainer
e88b4da53a Update README.md 2025-07-05 12:25:27 +08:00
geekgeekrun-maintainer
d218778690 Update README.md 2025-07-05 12:14:12 +08:00
geekgeekrun-maintainer
f903ef9175 Update README.md 2025-07-05 03:08:29 +08:00
geekgeekrun-maintainer
d114685116 Update README.md 2025-07-05 02:52:25 +08:00
geekgeekrun
bb30329d6c move backup filter condition to top 2025-07-04 21:44:50 +08:00
geekgeekrun
3683ead1d4 allow skip empty filter condition when set backup filter condition to search job 2025-07-04 21:25:47 +08:00
geekgeekrun
cac3e480e5 enhance intro text about job detail filter; adjust expectJobFilterTemplateList 2025-06-29 15:27:48 +08:00
geekgeekrun
52163342c3 add jobDetailRegExpMatchLogic option related flow in core script 2025-06-29 14:48:40 +08:00
geekgeekrun
8efce15fd5 enhance ui after add jobDetailRegExpMatchLogic option; change layout of job detail RegExp option 2025-06-29 05:30:39 +08:00
geekgeekrun
d5dcdb4bd1 add jobDetailRegExpMatchLogic option in UI 2025-06-29 04:32:28 +08:00
geekgeekrun
98b99c5d14 Merge branch 'master' into feature/ui 2025-06-29 02:19:54 +08:00
geekgeekrun-maintainer
d4fa72a28e Update README.md 2025-06-28 12:46:55 +08:00
40 changed files with 3168 additions and 548 deletions

View File

@@ -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'

View File

@@ -1,5 +1,7 @@
# 牛人快跑 - GeekGeekRun
**->维护者正在疯狂找工作<-**
一款可以帮助你在Boss直聘上**自动批量开聊Boss**的脚本基于Puppeteer。
与每一位牛人站在一起
@@ -12,33 +14,34 @@
## 程序有哪些功能?运行逻辑是什么?怎样使用?
### Boss炸弹
扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的Boss。
扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的Boss。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ebf5c51c-f52c-4426-a60a-97916f663c6a" />
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
#### 匹配步骤
1. 按照公司名称查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
2. 检查Boss活跃度
- 如果Boss活跃度为本月活跃或更往前的时间则会把职位标记为不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位
3. 对职位名称、职位类型、职位描述进行匹配
- 如果匹配则自动点击开聊按钮
- 不匹配则标记这个职位为不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位
1. 选择职位来源、职位筛选条件
<img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、Boss活跃度进行匹配
- 如果可以匹配,则自动点击开聊按钮
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
- 首先查看是否命中策略为“在Boss直聘上标记不合适”的配置如果命中则在Boss直聘上点击相关UI标记不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位对于“推荐列表中的职位”、“通过搜索找到的职位”此项会回落到“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”
- 然后查看是否命中策略为“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”的配置如果命中则仅在本地数据库中记录且本次运行将无视这个职位
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
#### 异常情况
- 当前页面筛选条件下,如果没有更多职位,则自动切换备选筛选条件,以获取更多新职位
- 当前页面筛选条件下,如果没有更多可投递职位,则自动切换筛选条件,以获取更多新职位;如果所有筛选条件搜索完没有更多可投递职位,则切换其它职位来源继续查找更多新职位;如果所有职位来源筛选完没有更多可投递职位,则等候一段时间后,会回到第一个职位来源的第一个筛选条件,从头开始查找职位
- 如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
#### 运行效果
> Boss直聘界面已改版如下是旧版Boss直聘界面
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
### 已读不回提醒器
Boss不明原因已读不回简历就是投不出去
@@ -118,12 +121,12 @@ Boss不明原因已读不回简历就是投不出去
```
1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置
## 关于软件不能正常启动、不能按预期运行的一些说明(朋友们帮忙测试一下吧)
## 关于软件不能正常启动、不能按预期运行的一些说明
本程序一直都在不断地更新迭代中包括新功能的增加以及Bug的修复。
然而作者本人只有 1台 macOS Apple Silicon设备、1台 Windows 设备且只有作者1人自测白天工作晚上熬夜开发、测试测试条件不是太好人力不足、设备不足测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时就会准出、发版。
然而作者本人只有 1台 macOS Apple Silicon设备、1台 Windows 设备且只有作者1人自测白天工作晚上熬夜开发、测试测试条件不是太好人力不足、设备不足测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时即认为可以准出、发版。
这将不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看问题也会尝试进行修复。
所以不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看问题也会尝试进行修复。
如果部分朋友超级喜欢写代码,或者本身工作就是写代码,可以帮忙下载 [Debugtron](https://github.com/pd4d10/debugtron/releases/tag/v1.0.0-alpha.0) 来调试本程序,查看反馈本程序的日志(如下图)。
@@ -135,10 +138,10 @@ Boss不明原因已读不回简历就是投不出去
### 个人而言
以我为例,我真的求职求累了
2023年初~2025年,这段时间
- 求职经历:离职求职5个月在职求职3个月曾面试66个岗位各种面试一面、二面、三面、HR面加起来134次Offer 寥寥无几
- 被动失业经历1 次裁员1 次卡试用期
- 目前状态:工作(但愿明天也有工作
2023年初~2025年,这段时间
- 求职经历:离职求职6个月曾面试66个岗位各种面试一面、二面、三面、HR面加起来134次Offer 寥寥无几
- 被动失业经历1 次裁员1 次卡试用期1 次部门收缩
- 目前状态:工作(**正在疯狂找工作**
如此多的经历已经让我成为了一位资深Boss用户
@@ -156,8 +159,8 @@ Boss不明原因已读不回简历就是投不出去
一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场
当然,如果运气好:
- 好不容易过了 HR 面,到了 Offer 阶段HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个 Offer
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了 Offer 入了职然而经过一段时间的接触最终结果完全不符合期望诸如“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活而且要帮前人擦屁股”、“领导希望用最小成本把你招来这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
- 好不容易过了 HR 面,到了 Offer 阶段HR / 用人部门 几乎都在极限压缩用人成本,经常能被 ** 到,但你又没有其它更好 Offer ,不敢贸然放弃这个不满意的 Offer
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了不满意的 Offer 入了职然而经过一段时间的接触最终结果完全不符合期望诸如“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活而且要帮前人擦屁股”、“领导希望用最小成本把你招来这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧
@@ -175,14 +178,22 @@ Boss不明原因已读不回简历就是投不出去
## 使用必读及免责声明
如下是使用必读及免责声明,请您务必逐条阅读;本程序首次启动时,您还会再看到一次;若您不接受如下提到的任何内容,请不要使用本程序。
- 本程序从某种程度上说属于辅助工具,与《<a href="https://about.zhipin.com/agreement/?id=registerprotocol_30" rel="noreferer noopener" target="blank">Boss直聘用户协议</a>》2023年3月版相关条款相违背您在注册Boss直聘时已签署过这一条款根据该条款`七、用户的平台使用义务`、`八、违约责任` 章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于**账号被强制退出登录**、**账号被限制使用**、**账号被封禁**等对您不利的风险;因此使用本程序即意味着**您愿意接受以上风险**,且如果相关风险发生,您需要自行承担相关后果,**本程序概不负责** 。
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的Boss直聘账号**进行求职。
- 本程序原理是模拟用户在Boss直聘网页上进行点击操作Boss直聘网站每过一段时间会发生改版且有可能包含A/B实验这将导致本程序相关脚本失效。如果您在使用过程中遇上程序未按照预期执行的情况请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
- 您的雇主可能会对您的计算机终端或网络进行监控,从而审计、跟踪您的行为;建议您**不要在您雇主提供的计算机终端或网络上使用本程序**。
- 本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘**不会泄露给第三方**也不会进行除自动开聊Boss以外的行为**请勿向他人泄漏您的Cookie**。
- 本程序**不会上报能够识别出您身份的信息****不会向您的雇主报告您的求职行为**
- 本程序**不对您的求职过程与结果负责**为您开聊的职位均在Boss直聘上发布请**自行甄别为您开聊的公司、认真决定是否参加面试、慎重选择Offer**
- 请在Boss直聘上自行**屏蔽您不期望投递的公司**。
- 本程序经历过了多次测试理论上来说大部分情况下可以正常运行如果您有顾虑建议通过VMWare WorkStation/Fusion、VirtualBox、Hyper-V/Windows沙盒等虚拟化技术运行本程序。如发生问题请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的Boss直聘账号**进行求职
- 本程序原理是模拟用户在Boss直聘网页上寻找关键元素并进行点击操作Boss直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈
- 您所在公司可能会对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;上级/HRBP团队可能会从 IT 团队处获取到监控数据,从而了解团队成员离职倾向。如果您不希望上级/HRBP团队了解到您正在求职建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及上级/HRBP报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**
- 本程序**没有内置任何付费功能****下载**、**使用**是**免费**的,任何人可以**免费获得**、**免费使用**。**作者没有利用本程序赚到过任何收入**。如果您是从GitHub以外的地方付费后“购买”的本程序或您被提示“必须付费后才能使用本程序”那**您大概率被骗了**,或者**您下载到了本程序修改版**。**本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方**。
- 本程序**不对您的求职过程与结果负责**为您开聊的职位均在Boss直聘上发布职位信息真实性由Boss直聘负责请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
- 请在Boss直聘上自行**屏蔽您不期望投递的公司**如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃请**在Boss直聘上屏蔽当前公司及与之关联的公司**。
- 本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过VMware Workstation/Fusion、Oracle VirtualBox、Microsoft Hyper-V等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
## 想联系作者?想反馈使用体验上的问题?想找求职搭子?
<img width="360" alt="QQ Group QRCode" src="https://github.com/user-attachments/assets/67560948-467b-44f5-a3e3-c8713255d8b7" />
欢迎进群聊天(刚建的群,人有点少,大胆聊就好)
--------
@@ -192,10 +203,12 @@ Boss不明原因已读不回简历就是投不出去
祝你求职成功,事业顺利,事事顺心
## Star 数据 感谢支持
## Star
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date" />
</picture>
感谢支持

View File

@@ -11,6 +11,7 @@
"author": "geekgeekrun",
"license": "ISC",
"dependencies": {
"cheerio": "1.0.0-rc.12",
"dayjs": "^1.11.10",
"json5": "^2.2.3",
"minimist": "^1.2.8",

View File

@@ -90,7 +90,7 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
//#region get count of combinations
// 计算符合限制条件的组合数量
export function calculateTotalCombinations(selectedFilters) {
export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) {
const {
salaryList = [],
experienceList = [],
@@ -106,8 +106,61 @@ export function calculateTotalCombinations(selectedFilters) {
const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
return [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
let result = [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
return accu * cur.length
}, 1)
if (!includeEmptyCondition) {
result -= 1
}
return result
}
//#endregion
export function checkAnyCombineBossRecommendFilterHasCondition(value) {
if (!Object.keys(value ?? {}).length) {
return false
}
return Object.keys(value).some((k) => {
return !!value[k]?.length
})
}
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 {
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({
salaryList: [],
experienceList: [],
degreeList: [],
scaleList: [],
industryList: []
})
}
return result
}

View File

@@ -1,3 +1,7 @@
export const activeDescList = [
'', "半年前活跃","近半年活跃","5月内活跃","4月内活跃","3月内活跃","2月内活跃","本月活跃","2周内活跃","本周活跃","3日内活跃","昨日活跃","今日活跃","刚刚活跃"
]
]
export const RECOMMEND_JOB_ENTRY_SELECTOR = `.c-expect-select a[ka="jobs_recommend_tab_click"]`
export const USER_SET_EXPECT_JOB_ENTRIES_SELECTOR = `.c-expect-select .expect-list .expect-item`
export const SEARCH_BOX_SELECTOR = `.c-search-input .search-input-box`

View File

@@ -1,4 +1,5 @@
{
"combineRecommendJobFilterType": 1,
"anyCombineRecommendJobFilter": {
"salaryList": [],
"experienceList": [],
@@ -6,6 +7,8 @@
"scaleList": [],
"industryList": []
},
"staticCombineRecommendJobFilterConditions": [],
"isSkipEmptyConditionForCombineRecommendJobFilter": false,
"expectJobRegExpStr": "",
"jobNotMatchStrategy": 1,
"jobNotActiveStrategy": 1,
@@ -13,6 +16,7 @@
"expectCityList": [],
"expectCityNotMatchStrategy": 3,
"strategyScopeOptionWhenMarkJobCityNotMatch": 2,
"jobDetailRegExpMatchLogic": 1,
"autoReminder": {
"throttleIntervalMinutes": 10,
"rechatLimitDay": 21,
@@ -21,5 +25,14 @@
"recentMessageQuantityForLlm": 8,
"rechatLlmFallback": 1,
"onlyRemindBossWithExpectJobType": true
}
},
"jobSourceList": [
{
"type": "expect",
"enabled": true
}
],
"isSageTimeEnabled": true,
"sageTimeOpTimes": 100,
"sageTimePauseMinute": 15
}

File diff suppressed because it is too large Load Diff

View 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++
}
}

View File

@@ -56,7 +56,10 @@ const main = async () => {
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
noPositionFoundForCurrentJob: new SyncHook(),
noPositionFoundAfterTraverseAllJob: new SyncHook(),
errorEncounter: new SyncHook(['errorInfo'])
errorEncounter: new SyncHook(['errorInfo']),
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']),
sageTimeEnter: new AsyncSeriesHook(['args']),
sageTimeExit: new AsyncSeriesHook(['args'])
}
initPlugins(hooks)
await hooks.daemonInitialized.callAsync()

View File

@@ -1,3 +1,4 @@
import { JobSource } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
@@ -29,4 +30,9 @@ export class ChatStartupLog {
nullable: true
})
autoStartupChatRecordId?: number;
@Column({
nullable: true
})
jobSource?: JobSource;
}

View File

@@ -0,0 +1,15 @@
import { JobHireStatus } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm();
@Entity()
export class JobHireStatusRecord {
@PrimaryColumn()
encryptJobId: string;
@Column()
hireStatus: JobHireStatus;
@Column()
lastSeenDate: Date;
}

View File

@@ -1,4 +1,4 @@
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
import { ChatStartupFrom } from "./ChatStartupLog";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
@@ -41,4 +41,9 @@ export class MarkAsNotSuitLog {
nullable: true
})
autoStartupChatRecordId?: number;
@Column({
nullable: true
})
jobSource?: JobSource;
}

View File

@@ -22,4 +22,26 @@ export enum StrategyScopeOptionWhenMarkJobNotMatch {
export enum SalaryCalculateWay {
MONTH_SALARY = 1,
ANNUAL_PACKAGE = 2,
}
export enum JobDetailRegExpMatchLogic {
EVERY = 1,
SOME = 2,
}
export enum JobSource {
expect = 1,
recommend = 2,
search = 3,
}
export enum CombineRecommendJobFilterType {
ANY_COMBINE = 1,
STATIC_COMBINE = 2,
}
export enum JobHireStatus {
HIRING = 1,
CLOSED = 2,
DELETED = 3,
}

View File

@@ -11,6 +11,7 @@ import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
import { LlmModelUsageRecord } from "./entity/LlmModelUsageRecord";
import { JobHireStatusRecord } from "./entity/JobHireStatusRecord";
function getBossInfoIfIsEqual (savedOne, currentOne) {
if (savedOne === currentOne) {
@@ -245,7 +246,7 @@ export async function saveChatStartupRecord(
ds: DataSource,
_jobInfo,
{ encryptUserId },
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined } = {}
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined, jobSource = undefined } = {}
) {
const { jobInfo } = _jobInfo;
@@ -256,7 +257,8 @@ export async function saveChatStartupRecord(
encryptCurrentUserId: encryptUserId,
encryptJobId: jobInfo.encryptId,
autoStartupChatRecordId,
chatStartupFrom
chatStartupFrom,
jobSource,
}
Object.assign(chatStartupLog, chatStartupLogPayload)
@@ -270,7 +272,7 @@ export async function saveMarkAsNotSuitRecord(
ds: DataSource,
_jobInfo,
{ encryptUserId },
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined } = {}
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined, jobSource = undefined } = {}
) {
const { jobInfo } = _jobInfo;
@@ -284,7 +286,8 @@ export async function saveMarkAsNotSuitRecord(
markFrom,
markReason,
extInfo: extInfo ? JSON.stringify(extInfo) : undefined,
markOp
markOp,
jobSource,
}
Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload)
@@ -333,9 +336,55 @@ export async function getNotSuitMarkRecordsInLastSomeDays (ds: DataSource, days
const result = await repo.findBy({
date: Raw(alias => `DATE(${alias}) >= DATE('${
new Date(
Number(new Date()) - 7 * 24 * 60 * 60 * 1000
Number(new Date()) - days * 24 * 60 * 60 * 1000
).toISOString()
}')`)
})
return result
}
export async function getChatStartupRecordsInLastSomeDays (ds: DataSource, days = 0) {
const repo = ds.getRepository(ChatStartupLog)
const result = await repo.findBy({
date: Raw(alias => `DATE(${alias}) >= DATE('${
new Date(
Number(new Date()) - days * 24 * 60 * 60 * 1000
).toISOString()
}')`)
})
return result
}
export async function getBossIdsByJobIds (ds: DataSource, jobIds: string[] = []) {
const repo = ds.getRepository(JobInfo)
const result = await repo.find({
where: jobIds.map(
id => ({
encryptJobId: id
})
)
})
return result
}
export async function saveJobHireStatusRecord(
ds: DataSource,
record: JobHireStatusRecord
) {
const jobHireStatusRecordRepository = ds.getRepository(JobHireStatusRecord);
await jobHireStatusRecordRepository.save(record);
return
}
export async function getJobHireStatusRecord(
ds: DataSource,
encryptJobId: string
) {
const repo = ds.getRepository(JobHireStatusRecord)
const result = await repo.findOne({
where: {
encryptJobId
}
})
return result
}

View File

@@ -1,7 +1,6 @@
import "reflect-metadata";
import { type DataSource } from "typeorm";
import { requireTypeorm } from "./utils/module-loader";
import fs from 'node:fs'
import { BossInfo } from "./entity/BossInfo";
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
@@ -21,15 +20,27 @@ import { VCompanyLibrary } from "./entity/VCompanyLibrary"
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
import { ChatMessageRecord } from './entity/ChatMessageRecord'
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
import { JobHireStatusRecord } from './entity/JobHireStatusRecord'
import sqlite3 from 'sqlite3';
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord, getNotSuitMarkRecordsInLastSomeDays } from "./handlers";
import {
saveChatStartupRecord,
saveJobInfoFromRecommendPage,
saveMarkAsNotSuitRecord,
getNotSuitMarkRecordsInLastSomeDays,
getChatStartupRecordsInLastSomeDays,
getBossIdsByJobIds,
saveJobHireStatusRecord
} from "./handlers";
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
import minimist from 'minimist'
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
import { JobHireStatus, MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
import { Init1000000000000 } from "./migrations/1000000000000-Init";
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
import { AddJobHireStatusTable1766466476822 } from "./migrations/1766466476822-AddJobHireStatusTable";
const lodashImportPromise = import('lodash-es')
export function initDb(dbFilePath) {
const { DataSource } = requireTypeorm()
@@ -59,12 +70,15 @@ export function initDb(dbFilePath) {
VMarkAsNotSuitLog,
ChatMessageRecord,
LlmModelUsageRecord,
JobHireStatusRecord,
],
migrations: [
Init1000000000000,
UpdateChatStartupLogTable1729182577167,
UpdateBossInfoTable1732032381304,
AddColumnForMarkAsNotSuitLog1746092370665,
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526,
AddJobHireStatusTable1766466476822
],
migrationsRun: true
});
@@ -111,6 +125,7 @@ export default class SqlitePlugin {
expectCityNotMatchStrategy,
blockJobNotSuit,
blockBossNotActive,
blockBossNotNewChat
}) => {
if (
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
@@ -118,8 +133,11 @@ export default class SqlitePlugin {
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
) {
const ds = await this.initPromise;
const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7) ?? []);
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7)) ?? [];
if (
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it =>
[
@@ -134,7 +152,10 @@ export default class SqlitePlugin {
id => blockJobNotSuit.add(id)
)
}
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
if (
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it => it.markReason === MarkAsNotSuitReason.BOSS_INACTIVE)
.map(
@@ -144,7 +165,10 @@ export default class SqlitePlugin {
id => blockJobNotSuit.add(id)
)
}
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
if (
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it => it.markReason === MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
.map(
@@ -154,6 +178,21 @@ export default class SqlitePlugin {
id => blockJobNotSuit.add(id)
)
}
const last30DayChatStartupRecords = (await getChatStartupRecordsInLastSomeDays(ds, 30)) ?? [];
const chattedJobIds = last30DayChatStartupRecords.map(it => it.encryptJobId)
if (chattedJobIds.length === 0) {
return
}
const { chunk } = await lodashImportPromise
const chattedJobIdChunks = chunk(chattedJobIds, 200)
const chattedBossIds = [];
for (const chattedJobIdChunk of chattedJobIdChunks) {
const chattedBossIdChunk = ((await getBossIdsByJobIds(ds, chattedJobIdChunk)) ?? []).map(it => it.encryptBossId)
chattedBossIds.push(...chattedBossIdChunk)
}
for (const id of chattedBossIds) {
blockBossNotNewChat.add(id)
}
}
}
);
@@ -163,22 +202,33 @@ export default class SqlitePlugin {
await saveJobInfoFromRecommendPage(ds, _jobInfo);
});
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList } = {}) => {
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async ({ jobInfo }) => {
const ds = await this.initPromise;
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId,
chatStartupFrom
return await saveJobHireStatusRecord(ds, {
encryptJobId: jobInfo.encryptId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
});
});
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined } = {}) => {
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList, jobSource = undefined } = {}) => {
const ds = await this.initPromise;
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId,
chatStartupFrom,
jobSource
});
});
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined, jobSource = undefined } = {}) => {
const ds = await this.initPromise;
return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId,
markFrom,
markReason,
extInfo,
markOp
markOp,
jobSource
});
});
}

View File

@@ -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> {
}
}

View File

@@ -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> {
}
}

View File

@@ -3,7 +3,7 @@ import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import { presetUno, presetAttributify, presetIcons } from 'unocss'
import transformerDirective from "@unocss/transformer-directives";
import transformerDirective from '@unocss/transformer-directives'
import Replace from 'unplugin-replace/vite'
process.env = { ...process.env, ...loadEnv(process.env.NODE_ENV!, process.cwd()) }
@@ -13,11 +13,17 @@ export default defineConfig({
rollupOptions: {
external: []
},
minify: 'terser'
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
},
plugins: [
externalizeDepsPlugin({
exclude: ['@geekgeekrun/geek-auto-start-chat-with-boss', '@geekgeekrun/dingtalk-plugin', '@geekgeekrun/utils', 'find-chrome-bin', '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension']
exclude: [
'@geekgeekrun/geek-auto-start-chat-with-boss',
'@geekgeekrun/dingtalk-plugin',
'@geekgeekrun/utils',
'find-chrome-bin',
'@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
]
}),
Replace({
delimiters: ['', ''],
@@ -26,11 +32,11 @@ export default defineConfig({
values: [
{
find: /<measurement_id>/g,
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string,
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string
},
{
find: /<api_secret>/g,
replacement: process.env.VITE_APP_GTAG_API_SECRET as string,
replacement: process.env.VITE_APP_GTAG_API_SECRET as string
}
]
})
@@ -39,7 +45,7 @@ export default defineConfig({
preload: {
plugins: [externalizeDepsPlugin()],
build: {
minify: 'terser'
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
}
},
renderer: {
@@ -52,11 +58,11 @@ export default defineConfig({
vue(),
UnoCSS({
presets: [presetUno(), presetAttributify(), presetIcons()],
transformers: [transformerDirective()],
transformers: [transformerDirective()]
})
],
build: {
minify: 'terser'
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
}
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "geekgeekrun-ui",
"version": "0.8.0",
"version": "0.12.0",
"description": "Boss 炸弹 - 自动开聊Boss助力每位打工人求职",
"main": "./out/main/index.js",
"author": "geekgeekrun",
@@ -38,13 +38,15 @@
"JSONStream": "^1.3.5",
"diff": "^7.0.0",
"electron-updater": "^6.1.7",
"lodash-es": "^4.17.21",
"minimist": "^1.2.8",
"mitt": "^3.0.1",
"node-machine-id": "^1.1.12",
"pinia": "^3.0.2",
"puppeteer": "20.1.0",
"puppeteer-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",
@@ -71,7 +73,6 @@
"eslint-plugin-vue": "^9.20.1",
"find-chrome-bin": "^2.0.1",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"normalize.css": "^8.0.1",
"prettier": "^3.2.4",
"sass": "^1.70.0",

View File

@@ -1,7 +1,7 @@
{
"version": "0.8.0",
"buildVersion": 15,
"buildTime": 1749349412239,
"buildHash": "9b64f05cb6205fc89bd3ad1c08a59993cd6ac127",
"version": "0.12.0",
"buildVersion": 26,
"buildTime": 1766591012661,
"buildHash": "0928b402fb5019b587ddec62825c9c2f4a5a11fb",
"name": "geekgeekrun-ui"
}

View File

@@ -1,5 +1,5 @@
import DingtalkPlugin from '@geekgeekrun/dingtalk-plugin/index.mjs'
import { app } from 'electron'
import { app, dialog } from 'electron'
import { SyncHook, AsyncSeriesHook } from 'tapable'
import {
readConfigFile,
@@ -93,7 +93,10 @@ const runAutoChat = async () => {
jobMarkedAsNotSuit: new AsyncSeriesHook(['positionInfoDetail', 'markDetail']),
noPositionFoundForCurrentJob: new SyncHook(),
noPositionFoundAfterTraverseAllJob: new SyncHook(),
errorEncounter: new SyncHook(['errorInfo'])
errorEncounter: new SyncHook(['errorInfo']),
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']),
sageTimeEnter: new AsyncSeriesHook(['args']),
sageTimeExit: new AsyncSeriesHook(['args'])
}
initPlugins(hooks)
@@ -120,6 +123,11 @@ const runAutoChat = async () => {
} catch (err) {
if (err instanceof Error) {
if (err.message.includes('LOGIN_STATUS_INVALID')) {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录Boss直聘`
})
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
break
}

View File

@@ -4,16 +4,26 @@ import {
readStorageFile,
writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import {
RECOMMEND_JOB_ENTRY_SELECTOR,
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR
} from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs'
import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
import {
saveJobInfoFromRecommendPage,
saveChatStartupRecord,
saveMarkAsNotSuitRecord,
saveChatMessageRecord
saveChatMessageRecord,
saveJobHireStatusRecord
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
import { initDb } from '@geekgeekrun/sqlite-plugin'
import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
import {
MarkAsNotSuitReason,
JobSource,
JobHireStatus
} from '@geekgeekrun/sqlite-plugin/dist/enums'
import cheerio from 'cheerio'
import fs from 'node:fs'
import { Target } from 'puppeteer'
@@ -39,13 +49,136 @@ const attachRequestsListener = async (target: Target) => {
return
}
function handleJobDetailPage({ encryptJobId } = { encryptJobId: null }) {
Promise.resolve()
.then(async () => {
if (!encryptJobId) {
return
}
try {
await page.waitForFunction(
({ encryptJobId }) => {
return (
location.href.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
document.readyState === 'complete'
)
},
undefined,
{ encryptJobId }
)
const htmlContent = await page.content()
if (htmlContent) {
const $ = cheerio.load(htmlContent)
const [jobBannerEl] = $('#main .job-banner') ?? []
if (!jobBannerEl) {
console.log(`access might be blocked`)
if (
htmlContent.includes(`您访问的页面不存在`) ||
location.href === `https://www.zhipin.com/`
) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.DELETED,
lastSeenDate: new Date()
})
}
} else {
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
if (jobStatusTextEl) {
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
if ([`职位已关闭`].includes(jobStatusText)) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.CLOSED,
lastSeenDate: new Date()
})
} else {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
}
}
}
} catch {
//
}
})
.catch(() => void 0)
}
if (page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
const encryptJobId = page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
if (encryptJobId) {
handleJobDetailPage({ encryptJobId })
}
}
async function getCurrentJobSource() {
const methodMap = {
async recommend() {
return await page.evaluate(
({ RECOMMEND_JOB_ENTRY_SELECTOR }) => {
return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active')
},
{
RECOMMEND_JOB_ENTRY_SELECTOR
}
)
},
async expect() {
return await page.evaluate(
({ USER_SET_EXPECT_JOB_ENTRIES_SELECTOR }) => {
return [...document.querySelectorAll(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)].some((el) =>
el.classList.contains('active')
)
},
{
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR
}
)
},
async search() {
const elHandle = await page.$(`.page-jobs-main`)
const currentKeyWord = await elHandle?.evaluate((el) => {
return el?.__vue__?.formData?.query
})
return !!currentKeyWord
}
}
for (const [type, func] of Object.entries(methodMap)) {
try {
if (await func()) {
return type
}
} catch (err) {
console.error('encounter error when get job source')
}
}
return null
}
page.on('response', async (response) => {
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
if (response.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
const encryptJobId = response
.url()
.match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
if (encryptJobId) {
handleJobDetailPage({ encryptJobId })
}
} else if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
const data = await response.json()
console.log(data)
if (data.code === 0) {
await saveJobInfoFromRecommendPage(await dbInitPromise, data.zpData)
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId: data.zpData.jobInfo.encryptId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
} else if (
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/reasons.json')
@@ -81,7 +214,7 @@ const attachRequestsListener = async (target: Target) => {
const reasonCodeToTextMap = await readStorageFile(
'job-not-suit-reason-code-to-text-cache.json'
)
const jobSource = await getCurrentJobSource()
const markDetail = {
markFrom: ChatStartupFrom.ManuallyFromRecommendList,
extInfo: {
@@ -90,12 +223,14 @@ const attachRequestsListener = async (target: Target) => {
text: reasonCodeToTextMap[chosenCode]
}
},
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON,
jobSource: JobSource[jobSource]
}
gtag('job_marked_as_not_suit', {
markFrom: markDetail.markFrom,
bossActiveTimeDesc: currentJobData?.bossInfo?.activeTimeDesc,
encryptJobId: currentJobData?.jobInfo?.encryptId
encryptJobId: currentJobData?.jobInfo?.encryptId,
jobSource: JobSource[jobSource]
})
if (reasonCodeToTextMap[chosenCode]?.includes('活跃度低')) {
markDetail.markReason = MarkAsNotSuitReason.BOSS_INACTIVE
@@ -131,9 +266,11 @@ const attachRequestsListener = async (target: Target) => {
const currentUserInfo = await page.evaluate(
'document.querySelector(".job-detail-box").__vue__.$store.state.userInfo'
)
const jobSource = await getCurrentJobSource()
gtag('new_chat_startup', {
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
encryptJobId: currentJobData?.jobInfo?.encryptId
encryptJobId: currentJobData?.jobInfo?.encryptId,
jobSource: JobSource[jobSource]
})
await saveChatStartupRecord(
await dbInitPromise,
@@ -142,7 +279,8 @@ const attachRequestsListener = async (target: Target) => {
encryptUserId: currentUserInfo.encryptUserId
},
{
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
jobSource: JobSource[jobSource]
}
)
} else if (
@@ -191,11 +329,15 @@ const attachRequestsListener = async (target: Target) => {
)
)?.filter(messageForSaveFilter) ?? []
const chatRecordList = rawChatRecordList.map(it => {
const chatRecordList = rawChatRecordList.map((it) => {
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
mappedItem.mid = it.mid
mappedItem.encryptFromUserId = it.isSelf ? currentUserInfo.encryptUserId : bossInfo.encryptBossId
mappedItem.encryptToUserId = it.isSelf ? bossInfo.encryptBossId : currentUserInfo.encryptUserId
mappedItem.encryptFromUserId = it.isSelf
? currentUserInfo.encryptUserId
: bossInfo.encryptBossId
mappedItem.encryptToUserId = it.isSelf
? bossInfo.encryptBossId
: currentUserInfo.encryptUserId
mappedItem.style = it.isSelf ? 'sent' : 'received'
mappedItem.type = it.type
mappedItem.time = it.time ? new Date(it.time) : null

View File

@@ -149,6 +149,32 @@ export default function initIpc() {
bossConfig.strategyScopeOptionWhenMarkJobWorkExpNotMatch =
payload.strategyScopeOptionWhenMarkJobWorkExpNotMatch
}
if (hasOwn(payload, 'jobDetailRegExpMatchLogic')) {
bossConfig.jobDetailRegExpMatchLogic = payload.jobDetailRegExpMatchLogic
}
if (hasOwn(payload, 'isSkipEmptyConditionForCombineRecommendJobFilter')) {
bossConfig.isSkipEmptyConditionForCombineRecommendJobFilter =
payload.isSkipEmptyConditionForCombineRecommendJobFilter
}
if (hasOwn(payload, 'jobSourceList')) {
bossConfig.jobSourceList = payload.jobSourceList
}
if (hasOwn(payload, 'combineRecommendJobFilterType')) {
bossConfig.combineRecommendJobFilterType = payload.combineRecommendJobFilterType
}
if (hasOwn(payload, 'staticCombineRecommendJobFilterConditions')) {
bossConfig.staticCombineRecommendJobFilterConditions =
payload.staticCombineRecommendJobFilterConditions
}
if (hasOwn(payload, 'isSageTimeEnabled')) {
bossConfig.isSageTimeEnabled = payload.isSageTimeEnabled
}
if (hasOwn(payload, 'sageTimeOpTimes')) {
bossConfig.sageTimeOpTimes = payload.sageTimeOpTimes
}
if (hasOwn(payload, 'sageTimePauseMinute')) {
bossConfig.sageTimePauseMinute = payload.sageTimePauseMinute
}
promiseArr.push(writeConfigFile('boss.json', bossConfig))

View File

@@ -34,7 +34,10 @@ export async function launchBoss(browser: Browser) {
}
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
try {
await Promise.all([page.goto(bossChatUiUrl, { timeout: 0 }), page.waitForNavigation()])
await Promise.all([
page.goto(bossChatUiUrl, { timeout: 0 }),
page.waitForNavigation({ timeout: 120 * 1000 })
])
} catch (error) {
if (error?.message?.startsWith('net::ERR_INTERNET_DISCONNECTED')) {
throw new Error('ERR_INTERNET_DISCONNECTED')

View File

@@ -3,6 +3,7 @@ import { MsgStatus, type ChatListItem } from './types'
import { Browser, Page } from 'puppeteer'
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
import { app, dialog } from 'electron'
import { initDb } from '@geekgeekrun/sqlite-plugin'
@@ -11,7 +12,11 @@ import {
readConfigFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatMessageRecord'
import { saveChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers'
import {
saveChatMessageRecord,
getJobHireStatusRecord,
saveJobHireStatusRecord
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
import { writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import * as fs from 'fs'
import { pipeWriteRegardlessError } from '../utils/pipe'
@@ -19,6 +24,9 @@ import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
import { messageForSaveFilter } from '../../../common/utils/chat-list'
import { RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat'
import gtag from '../../utils/gtag'
import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums';
import dayjs from 'dayjs'
import cheerio from 'cheerio'
const throttleIntervalMinutes =
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
@@ -104,6 +112,88 @@ async function saveCurrentChatRecord(page) {
await saveChatMessageRecord(ds, chatRecordList)
}
async function checkJobIsClosed() {
const encryptJobId = await pageMapByName.boss!.evaluate(() => {
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$.encryptJobId
})
if (!encryptJobId) {
return false
}
let isJobClosed = false
let record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
// not seen before, or last seen more than 6 hours ago
// fetch new status
if (
!record ||
(record.hireStatus === JobHireStatus.HIRING &&
Date.now() - Number(dayjs(record.lastSeenDate)) > 6 * 60 * 60 * 1000)
) {
const positionNameElHandle = await pageMapByName.boss!.$(
`#main .chat-conversation [ka="geek_chat_job_detail"] .right-content`
)
if (!positionNameElHandle) {
return false
}
positionNameElHandle.click()
try {
const targetPage = await waitForPage(pageMapByName.boss!.browser(), async (page) => {
const url = page.url()
if (
url.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
(await page.evaluate(() => document.readyState === 'complete'))
) {
return true
}
return false
})
const htmlContent = await targetPage.content()
if (htmlContent) {
const $ = cheerio.load(htmlContent)
const [jobBannerEl] = $('#main .job-banner') ?? []
if (!jobBannerEl) {
console.log(`access might be blocked`)
if (
htmlContent.includes(`您访问的页面不存在`) ||
location.href === `https://www.zhipin.com/`
) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.DELETED,
lastSeenDate: new Date()
})
}
} else {
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
if (jobStatusTextEl) {
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
if ([`职位已关闭`].includes(jobStatusText)) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.CLOSED,
lastSeenDate: new Date()
})
} else {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
}
}
}
targetPage.close().catch(() => void 0)
} catch (err) {
console.log(`checkJobIsClosed error: ${err}`)
}
record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
}
if (record.hireStatus !== JobHireStatus.HIRING) {
isJobClosed = true
}
return isJobClosed
}
let browser: null | Browser = null
const mainLoop = async () => {
if (browser) {
@@ -201,7 +291,9 @@ const mainLoop = async () => {
(rechatLimitDay && it.updateTime
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
: true) &&
((((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) ||
((((it.lastIsSelf &&
it.lastMsgStatus === MsgStatus.HAS_READ &&
!it.lastText.includes('你撤回了')) ||
canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) &&
!it.unreadCount) ||
(!it.lastIsSelf && it.lastText === '开场问题,期待你的回答'))
@@ -301,7 +393,9 @@ const mainLoop = async () => {
)?.filter(messageForSaveFilter) ?? []
const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0
const isJobClosed = await checkJobIsClosed()
if (
!isJobClosed &&
isExpectJobTypeMatch &&
historyMessageList[historyMessageList.length - 1].isSelf &&
historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ &&

View File

@@ -2,26 +2,40 @@ import gtag from '.'
export default class GtagPlugin {
apply(hooks) {
hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom }) => {
hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom, jobSource }) => {
gtag('new_chat_startup', {
chatStartupFrom,
encryptJobId: jobData.jobInfo.encryptId
})
})
hooks.jobMarkedAsNotSuit.tap('GtagPlugin', (jobData, { markFrom, markOp, markReason }) => {
gtag('job_marked_as_not_suit', {
markFrom,
markOp,
markReason,
bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc,
encryptJobId: jobData.jobInfo.encryptId
encryptJobId: jobData.jobInfo.encryptId,
jobSource
})
})
hooks.jobMarkedAsNotSuit.tap(
'GtagPlugin',
(jobData, { markFrom, markOp, markReason, jobSource }) => {
gtag('job_marked_as_not_suit', {
markFrom,
markOp,
markReason,
bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc,
encryptJobId: jobData.jobInfo.encryptId,
jobSource
})
}
)
hooks.noPositionFoundForCurrentJob.tap('GtagPlugin', () => {
gtag('no_position_found_for_current_job')
})
hooks.noPositionFoundAfterTraverseAllJob.tap('GtagPlugin', () => {
gtag('no_position_found_after_traverse_all_job')
})
hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => {
gtag('encounter_empty_rec_job_list', { pageQuery })
})
hooks.sageTimeEnter.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
gtag('sage_time_enter', { tag, totalEnabledTimes, recordedOpCount })
})
hooks.sageTimeExit.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
gtag('sage_time_exit', { tag, totalEnabledTimes, recordedOpCount })
})
}
}

View File

@@ -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>

View File

@@ -0,0 +1,272 @@
<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="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 { 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>

View File

@@ -109,8 +109,14 @@ import { ElForm, ElMessage } from 'element-plus'
import { ref, onUnmounted, onMounted } from 'vue'
import { checkCookieListFormat } from '../../../../common/utils/cookie'
import { useRouter } from 'vue-router'
import { gtagRenderer } from '@renderer/utils/gtag'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'cookie-assistant',
...params
})
}
const router = useRouter()
const cookieInvalid = ref(false)

View File

@@ -1,5 +1,5 @@
<template>
<div class="first-run-readme" ref="componentRootEl">
<div ref="componentRootEl" class="first-run-readme">
<div class="first-run-readme__inner">
<div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div>
<div class="readme-desc">
@@ -12,25 +12,32 @@
>
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
本程序从某种程度上说属于辅助工具<el-link
@click="
electron.ipcRenderer.send(
'open-external-link',
'https://about.zhipin.com/agreement/'
)
color-blue
@click.stop.prevent="
() => {
electron.ipcRenderer.send(
'open-external-link',
'https://about.zhipin.com/agreement/'
)
gtagRenderer('view_boss_agreement_clicked')
}
"
>Boss直聘用户协议</el-link
>相关条款相违背
根据该条款如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b
class="color-red"
>账号被强制退出登录</b
><b class="color-red">账号被限制使用</b><b class="color-red">账号被封禁</b
>2023年3月版相关条款相违背您在注册Boss直聘时已签署过这一条款根据该条款
<i>用户的平台使用义务</i><i>违约责任</i>
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录账号被限制使用账号被封禁</b
>等对您不利的风险因此使用本程序即意味着<b class="color-red">您愿意接受以上风险</b
>且如果相关风险发生<b class="color-red">您需要自行承担相关后果</b><b
class="color-red"
>本程序不负责</b
>
>且如果相关风险发生<b class="color-red">您需要自行承担相关后果本程序不负责</b>
</ElCheckbox>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊Boss以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b
class="color-red"
>注意节制</b
@@ -38,39 +45,58 @@
>注册一个本程序专用的新的Boss直聘账号</b
>进行求职
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序原理是模拟用户在Boss直聘网页上进行点击操作Boss直聘网站每过一段时间会发生改版且有可能包含A/B实验这将导致本程序相关脚本失效此时您可以点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
您的雇主可能会对您的计算机终端或网络进行监控从而审计跟踪您的行为建议您<b
本程序原理是模拟用户在Boss直聘网页上寻找关键元素并进行点击操作Boss直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red"
>不要在您雇主提供的计算机终端或网络上使用本程序</b
>
>浏览器重复闪退重新启动</b
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊Boss以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
您所在公司可能会对您的计算机终端或网络进行<b color-red>监控</b>从而<b color-red
>审计跟踪</b
>您的行为上级 / HRBP 团队可能会从 IT
团队处获取到监控数据从而了解团队成员离职倾向如果您不希望上级 / HRBP
团队了解到您正在求职建议您<b color-red
>不要在您所在公司提供的计算机终端或网络上使用本程序</b
>
</ElCheckbox>
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
本程序<b class="color-red"
>不会上报您的隐私诸如能够识别出您身份的信息不会向您的雇主报告您的求职行为</b
>如果您在使用过程中遇上程序错误您可以点击程序左下角进行反馈
本程序尊重您的隐私<b color-red
>不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及上级 / HRBP
团队报告您的求职行为不会向猎头公司泄露您的信息</b
>
</ElCheckbox>
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
<b class="color-red">本程序不对您的求职过程与结果负责</b
>为您开聊的职位均在Boss直聘上发布<b class="color-red">自行甄别为您开聊的公司</b
><b class="color-red">认真决定是否参加面试</b><b class="color-red">慎重选择Offer</b
本程序<b class="color-red">没有内置任何付费功能</b><b class="color-red"
>下载使用是免费的</b
>任何人可以<b class="color-red">免费获得免费使用</b><b class="color-red"
>作者没有利用本程序赚到过任何收入</b
>如果您是从GitHub以外的地方付费后购买的本程序或您被提示必须付费后才能使用本程序<b
class="color-red"
>那您大概率被骗了</b
>或者<b class="color-red">您下载到了本程序修改版</b><b class="color-red"
>本程序对此概不负责请勿找作者商讨退款售后事宜相关事宜请咨询卖方</b
>
</ElCheckbox>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b>
本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在Boss直聘上发布职位信息真实性由Boss直聘负责<b
class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
>
</ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行如果您有顾虑建议通过VMWare
WorkStation/FusionVirtualBoxHyper-V/Windows沙盒等虚拟化技术运行本程序如发生问题请点击程序左下角进行反馈
请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃<b
class="color-red"
>在Boss直聘上屏蔽当前公司及与之关联的公司</b
>
</ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过
VMware Workstation / FusionOracle VirtualBoxMicrosoft Hyper-V
等虚拟化技术运行本程序如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
</ElCheckboxGroup>
</article>
@@ -86,26 +112,36 @@
<script lang="ts" setup>
import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus'
import { ref, onMounted, onBeforeMount } from 'vue';
import { gtagRenderer } from '@renderer/utils/gtag'
import { ref, onMounted, onBeforeMount } from 'vue'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'first-run-agreement',
...params
})
}
const electron = window.electron
const readmeItemCheckStatusList = ref<number[]>([])
const handleCancel = () => {
const handleCancel = async () => {
gtagRenderer('cancel_clicked')
await sleep(500)
electron.ipcRenderer.invoke('exit-app-immediately')
}
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
const handleSubmit = () => {
gtagRenderer('submit_clicked')
const COUNT = 9
const COUNT = 10
if (readmeItemCheckStatusList.value.length !== COUNT) {
gtagRenderer('agreement_not_finish_read_tip_displayed')
ElMessage.warning(
`您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`
)
ElMessage.warning({
message: `您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`,
grouping: true
})
unreadItemsAfterClickSubmit.value = {}
for (let i = 0; i < COUNT; i++) {
if (!readmeItemCheckStatusList.value.includes(i)) {
@@ -127,13 +163,10 @@ const handleReadmeItemCheckStatusListChange = (value: number[]) => {
const componentRootEl = ref<HTMLElement>()
onMounted(() => {
const ro = new ResizeObserver(() => {
electron.ipcRenderer.send(
'update-window-size',
{
width: componentRootEl.value!.offsetWidth,
height: componentRootEl.value!.offsetHeight,
}
)
electron.ipcRenderer.send('update-window-size', {
width: componentRootEl.value!.offsetWidth,
height: componentRootEl.value!.offsetHeight
})
})
ro.observe(componentRootEl.value!)
onBeforeMount(() => {

View File

@@ -254,7 +254,7 @@ import {
} from 'element-plus'
import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue'
import { ref, onMounted, watch, nextTick, computed } from 'vue'
import { gtagRenderer } from '@renderer/utils/gtag'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../../common/constant'
import { v4 as uuid } from 'uuid'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
@@ -267,6 +267,13 @@ interface LlmConfigItem {
enabled: true
}
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'llm-config',
...params
})
}
function getNewConfigItem(): LlmConfigItem {
return {
id: uuid(),

View File

@@ -0,0 +1,233 @@
import { JobDetailRegExpMatchLogic } from '@geekgeekrun/sqlite-plugin/src/enums'
const expectJobFilterTemplateList = [
{
type: '不限职位',
name: '不限职位(随便投)',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '互联网/AI',
name: 'Java',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '\\bJava\\b',
expectJobDescRegExpStr:
'\\bJava\\b|JVM|消息队列|MQ|SQL|Oracle|MongoDB|Redis|Nginx|Dubbo|Docker|K8s|Kubernetes',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '互联网/AI',
name: 'Golang',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '\\bGolang\\b',
expectJobDescRegExpStr:
'\\bGo\\b|\\bGolang\\b|消息队列|MQ|SQL|Oracle|MongoDB|Redis|Nginx|Dubbo|Docker|K8s|Kubernetes',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '互联网/AI',
name: '前端开发工程师',
config: {
expectJobNameRegExpStr: '前端|H5|\\bFE\\b',
expectJobTypeRegExpStr: '前端开发|javascript',
expectJobDescRegExpStr: '前端|vue|react|node|\\bjs\\b|javascript|H5',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '互联网/AI',
name: '前端开发工程师(不考虑外包、兼职)',
config: {
expectJobNameRegExpStr: '^(?=.*(前端|H5|\\bFE\\b))(?!.*(?:外包|驻场|外派|兼职|短期))',
expectJobTypeRegExpStr: '前端开发|javascript',
expectJobDescRegExpStr:
'^(?=.*(前端|vue|react|node|\\bjs\\b|javascript|H5))(?!.*(?:外包|驻场|外派|兼职|短期))',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '互联网/AI',
name: '测试工程师、测试开发',
config: {
expectJobNameRegExpStr: '测试|测开|QA|质量',
expectJobTypeRegExpStr: '测试工程师|测试开发',
expectJobDescRegExpStr:
'测试|测开|QA|线上问题|自动化|复盘|效率|Selenium|Puppeteer|Playwright|Cypress|JMeter|LoadRunner|QTP|TestNG|JUnit|Pytest|Fiddler|Charles|Jenkins|Appium|黑盒|白盒|用例|缺陷|Linux|Ubuntu|Debian|CentOS|Shell|c\\+\\+|Python|PHP|\\bJava\\b|Node|\\bGo\\b|\\bGolang\\b',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '互联网/AI',
name: '运维工程师、运维开发工程师',
config: {
expectJobNameRegExpStr: '运维(开发)?|SRE',
expectJobTypeRegExpStr: '运维(开发)?工程师',
expectJobDescRegExpStr:
'运维|SRE|服务器|云计算|Docker|K8s|Kubernetes|Linux|Ubuntu|Debian|CentOS|Shell|Python|\\bGo\\b|\\bGolang\\b|监控|Prometheus|Grafana|ELK|负载均衡|部署|Nginx|Apache|DevOps|harbor',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '互联网/AI',
name: '数据开发',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '数据开发',
expectJobDescRegExpStr: 'c\\+\\+|Python|\\bGo\\b|\\bGolang\\b|\\bJava\\b|Node|数据仓库|ETL|大数据|Hadoop|Spark|Flink|Hive|Presto|数据湖|数仓|SQL|Oracle|MongoDB|Redis|Kafka',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '互联网/AI',
name: '实施工程师',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '实施',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '产品',
name: '产品经理',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '产品经理',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '产品',
name: '用户研究',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '用户研究',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '产品',
name: '游戏策划',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '游戏策划',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '客服/运营',
name: '产品运营',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '产品运营',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '客服/运营',
name: '用户运营',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '用户运营',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '客服/运营',
name: '数据标注/AI训练师',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '数据标注|AI训练师',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '财务/审计/税务',
name: '会计、出纳',
config: {
expectJobNameRegExpStr: '会计|Accountant|出纳|财务',
expectJobTypeRegExpStr: '会计|出纳',
expectJobDescRegExpStr:
'会计|财务|出纳|审计|账务|税务|总账|做账|应付|应收|成本|资产|资金|记账|发票|结算|核算|汇算|利润|对账|报税|回款|SAP|用友|金蝶',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '人力/行政/法务',
name: '人力资源专员/助理、人力资源经理/主管',
config: {
expectJobNameRegExpStr: 'HR|人力|人资|人事',
expectJobTypeRegExpStr: '人力',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '人力/行政/法务',
name: 'HRBP',
config: {
expectJobNameRegExpStr: 'BP|HRG|HR|人力|人资|人事',
expectJobTypeRegExpStr: 'HRBP|人力',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '人力/行政/法务',
name: '员工关系',
config: {
expectJobNameRegExpStr: '员工关系|劳动关系|SSC|社保|HR|人力|人资|人事',
expectJobTypeRegExpStr: '员工关系|人力',
expectJobDescRegExpStr: '员工关系|劳动关系|SSC|社保|考勤|入职|离职|入转调离',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '人力/行政/法务',
name: '招聘',
config: {
expectJobNameRegExpStr: '招聘|高招|Recruiter|HR|人力|人资|人事',
expectJobTypeRegExpStr: '招聘|猎头|人力',
expectJobDescRegExpStr:
'招聘|高招|Recruiter|简历|面试|人才引进|Mapping|人才画像|offer|猎头|内推|外推|猎聘|Boss|拉勾|前程无忧|智联|58同城|领英|LinkedIn|ATS|人才库|Moka|北森|iTenant|倍罗|大易',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY
}
},
{
type: '人力/行政/法务',
name: '薪酬绩效',
config: {
expectJobNameRegExpStr: '薪酬|绩效|福利|COE|payroll',
expectJobTypeRegExpStr: '薪酬绩效',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
type: '人力/行政/法务',
name: '企业文化',
config: {
expectJobNameRegExpStr: '企业文化|组织文化|组织|OC|廉洁|反腐',
expectJobTypeRegExpStr: '企业文化',
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
]
export default expectJobFilterTemplateList

View File

@@ -231,10 +231,15 @@ import {
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK
} from '../../../../common/enums/auto-start-chat'
import { gtagRenderer } from '@renderer/utils/gtag'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import mittBus from '../../utils/mitt'
import { QuestionFilled } from '@element-plus/icons-vue'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'rnrr-config',
...params
})
}
const router = useRouter()
const formContent = ref({
autoReminder: {

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex h100vh">
<div class="flex flex-col w200px pt30px pl30px aside-nav of-hidden">
<div class="flex flex-col min-w200px w200px pt30px pl30px aside-nav of-hidden">
<div class="nav-list flex-1 of-auto">
<RouterLink to="./GeekAutoStartChatWithBoss">
Boss炸弹

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

View File

@@ -322,11 +322,18 @@
import { ElForm, ElButton, ElAlert, ElMessageBox } from 'element-plus'
import { ref, onMounted, computed } from 'vue'
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
import { gtagRenderer } from '@renderer/utils/gtag'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { type ResumeContent, resumeContentEnoughDetect } from '../../../../common/utils/resume'
const formRef = ref<InstanceType<typeof ElForm>>()
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'resume-editor',
...params
})
}
const getEmptyFormContent = () => {
const o: any = {
expectJob: '',

View File

@@ -0,0 +1,32 @@
export async function waitForPage(browser, predicate, {
polling = 1000, timeout = 10 * 1000
} = {}) {
let intervalTimer = null
let timeoutTimer = null
return await new Promise((resolve, reject) => {
timeoutTimer = setTimeout(
() => {
clearInterval(intervalTimer)
clearInterval(timeoutTimer)
reject(
new Error(`waitForPage timeout after ${timeout} ms`)
)
},
timeout
)
intervalTimer = setInterval(async () => {
const allPages = await browser.pages()
let target = null
for (let i = allPages.length - 1; i >= 0; i--) {
const currentPage = allPages[i]
if (
await predicate(currentPage)
) {
clearInterval(intervalTimer)
clearInterval(timeoutTimer)
resolve(currentPage)
}
}
}, polling)
})
}

121
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
cheerio:
specifier: 1.0.0-rc.12
version: 1.0.0-rc.12
dayjs:
specifier: ^1.11.10
version: 1.11.10
@@ -129,6 +132,9 @@ importers:
electron-updater:
specifier: ^6.1.7
version: 6.1.7
lodash-es:
specifier: ^4.17.21
version: 4.17.21
minimist:
specifier: ^1.2.8
version: 1.2.8
@@ -150,6 +156,9 @@ importers:
uuid:
specifier: ^11.1.0
version: 11.1.0
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.4.15)
devDependencies:
'@electron-toolkit/eslint-config':
specifier: ^1.0.2
@@ -223,9 +232,6 @@ importers:
js-yaml:
specifier: ^4.1.0
version: 4.1.0
lodash-es:
specifier: ^4.17.21
version: 4.17.21
normalize.css:
specifier: ^8.0.1
version: 8.0.1
@@ -2638,7 +2644,6 @@ packages:
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: true
/boolean@3.2.0:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
@@ -2809,6 +2814,30 @@ packages:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true
/cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
dependencies:
boolbase: 1.0.0
css-select: 5.2.2
css-what: 6.2.2
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
dev: false
/cheerio@1.0.0-rc.12:
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
engines: {node: '>= 6'}
dependencies:
cheerio-select: 2.1.0
dom-serializer: 2.0.0
domhandler: 5.0.3
domutils: 3.2.2
htmlparser2: 8.0.2
parse5: 7.3.0
parse5-htmlparser2-tree-adapter: 7.1.0
dev: false
/chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -3049,6 +3078,16 @@ packages:
which: 2.0.2
dev: true
/css-select@5.2.2:
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
dependencies:
boolbase: 1.0.0
css-what: 6.2.2
domhandler: 5.0.3
domutils: 3.2.2
nth-check: 2.1.1
dev: false
/css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@@ -3057,6 +3096,11 @@ packages:
source-map-js: 1.0.2
dev: true
/css-what@6.2.2:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
dev: false
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -3248,6 +3292,33 @@ packages:
esutils: 2.0.3
dev: true
/dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
dev: false
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false
/domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: false
/domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: false
/dotenv-expand@5.1.0:
resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==}
dev: true
@@ -3414,6 +3485,11 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
/entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
dev: false
/env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -4159,6 +4235,15 @@ packages:
lru-cache: 6.0.0
dev: true
/htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 4.5.0
dev: false
/http-cache-semantics@4.1.1:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
@@ -4567,7 +4652,6 @@ packages:
/lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: true
/lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
@@ -5044,7 +5128,6 @@ packages:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
@@ -5191,6 +5274,13 @@ packages:
parse5: 6.0.1
dev: false
/parse5-htmlparser2-tree-adapter@7.1.0:
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
dependencies:
domhandler: 5.0.3
parse5: 7.3.0
dev: false
/parse5@5.1.1:
resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==}
dev: false
@@ -5199,6 +5289,12 @@ packages:
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
dev: false
/parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
dependencies:
entities: 6.0.1
dev: false
/path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
dev: true
@@ -5821,6 +5917,10 @@ packages:
ip: 2.0.0
smart-buffer: 4.2.0
/sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
dev: false
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
@@ -6625,6 +6725,15 @@ packages:
'@vue/shared': 3.4.15
typescript: 5.3.3
/vuedraggable@4.1.0(vue@3.4.15):
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
peerDependencies:
vue: ^3.0.1
dependencies:
sortablejs: 1.14.0
vue: 3.4.15(typescript@5.3.3)
dev: false
/web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}