Compare commits

...
109 Commits
Author SHA1 Message Date
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
geekgeekrun b024c6b517 ui-v0.8.0 2025-06-08 10:23:32 +08:00
geekgeekrun 9b64f05cb6 Merge branch 'feature/ui' 2025-06-08 10:22:48 +08:00
geekgeekrun eefb25a3da enhance ui appearance 2025-06-08 10:20:07 +08:00
geekgeekrun 2dad39c8e4 disable 在校生 option of work exp 2025-06-03 00:55:56 +08:00
geekgeekrun 699bdd2641 add filter logic of job salary in script 2025-06-03 00:55:44 +08:00
geekgeekrun 48f1232f7d adjust priority of mark as not suit strategy when a job match more than one condition 2025-06-02 21:53:12 +08:00
geekgeekrun 2409699ccf add filter logic of job work exp in script 2025-06-02 16:26:31 +08:00
geekgeekrun 64c007066e fix ui display 2025-06-02 11:05:43 +08:00
geekgeekrun bd503eb268 add work experience filter ui 2025-06-01 23:20:18 +08:00
geekgeekrun 8dc8ca825d add salary filter ui 2025-06-01 21:41:09 +08:00
geekgeekrun-maintainer 8e8fb6d331 Update README.md 2025-06-01 14:59:54 +08:00
geekgeekrun 2f3a557fb5 update element-plus 2025-05-31 15:52:15 +08:00
geekgeekrun 780cd73e46 Merge remote-tracking branch 'origin/master' 2025-05-30 23:40:07 +08:00
geekgeekrun-maintainer 6c05128e89 Update README.md 2025-05-29 23:42:23 +08:00
geekgeekrun e235ae099b ui-v0.7.1 2025-05-29 03:09:57 +08:00
geekgeekrun d479687b8b Merge branch 'feature/ui' 2025-05-29 03:08:01 +08:00
geekgeekrun 4627b4218e fix commandline args 2025-05-29 02:41:38 +08:00
geekgeekrun b64f411e4a add edge to block list due to https://github.com/geekgeekrun/geekgeekrun/issues/5 2025-05-29 01:55:35 +08:00
geekgeekrun 12ff9dc4ba adjust commandline; adjust base url of download browser 2025-05-29 01:42:23 +08:00
geekgeekrun 959c32eb63 Merge branch 'master' into feature/ui 2025-05-28 23:52:42 +08:00
geekgeekrun ce38ecea55 ui-v0.7.0 2025-05-28 03:54:22 +08:00
geekgeekrun 340f8946b9 Merge branch 'feature/ui' 2025-05-28 03:50:26 +08:00
geekgeekrun 59002f73e9 add intro for how to fill detail filter 2025-05-28 03:49:05 +08:00
geekgeekrun 5626cf6721 add guide for stop trace one boss 2025-05-28 02:23:50 +08:00
geekgeekrun fe1c85b38d add comment for expectCityNotMatchStrategy === MarkAsNotSuitOp.NO_OP 2025-05-27 02:04:50 +08:00
geekgeekrun 24018f63b5 enhance ui text 2025-05-27 01:38:30 +08:00
geekgeekrun 5effcc27ab add strategyScopeOptionWhenMarkJobCityNotMatch option; change default value of expectCityNotMatchStrategy to noop; enhance ui 2025-05-27 01:25:39 +08:00
geekgeekrun aee6340c6a show mark as not suit for city reason in not suit record table 2025-05-25 22:45:13 +08:00
geekgeekrun 03df5f8d5a add city not suit mark in main process 2025-05-25 13:30:44 +08:00
geekgeekrun 7d3a0e80d4 enhance ui appearance of auto start chat config 2025-05-25 01:30:32 +08:00
geekgeekrun 10f1f150bf enhance ui appearance of auto start chat config 2025-05-25 00:41:09 +08:00
geekgeekrun 64249593b8 add basic ui for expectCityList config 2025-05-24 18:30:48 +08:00
geekgeekrun 217b2bbf2a move runMode from env to args. remove support of cli mode. 2025-05-24 14:09:57 +08:00
geekgeekrun-maintainer 02667fe2f8 Update README.md 2025-05-24 14:09:57 +08:00
geekgeekrun 5539842392 Merge branch 'feature/ui' of github.com-geekgeekrun:geekgeekrun/geekgeekrun into feature/ui 2025-05-21 02:17:38 +08:00
geekgeekrun b57e646147 move runMode from env to args. remove support of cli mode. 2025-05-21 02:17:27 +08:00
geekgeekrun-maintainer 6982f6028d Update README.md 2025-05-17 17:38:40 +08:00
geekgeekrun f4cd4b884b ui-v0.6.1 2025-05-10 23:39:43 +08:00
geekgeekrun 61c28ae164 fix error popped when first run and init db 2025-05-10 23:04:07 +08:00
geekgeekrun ece7fc61e3 ui-v0.6.0 2025-05-08 03:35:30 +08:00
geekgeekrun 97793adb0f check message list when find last message is 开场问题,期待你的回答 2025-05-08 03:33:23 +08:00
geekgeekrun b5c3116c01 add onlyRemindBossWithExpectJobType option for autoReminder 2025-05-06 06:31:14 +08:00
geekgeekrun 10cd1cf11e ui-v0.5.0 2025-05-03 16:50:54 +08:00
geekgeekrun 7d2e60dfd0 update company list 2025-05-03 16:23:37 +08:00
geekgeekrun 6986fbdc3c disable synchronize when typeorm db init, all schema change depend on migration 2025-05-03 15:03:25 +08:00
geekgeekrun 5174da2c6c add tip about wrong mark as not suit 2025-05-03 03:48:52 +08:00
geekgeekrun 043d782a05 add logic support customize job no active time range. add standalone logic to handle no active job. fix logic of set tempTargetJobIndexToCheckDetail 2025-05-03 03:31:39 +08:00
geekgeekrun 67e4b94f9e add logic to handle if mark as not suit when job detail is not match 2025-05-01 22:01:05 +08:00
geekgeekrun-maintainer 45d14e70fb Update README.md 2025-04-30 00:23:40 +08:00
geekgeekrun-maintainer 44202b4685 Update README.md 2025-04-29 23:51:41 +08:00
geekgeekrun-maintainer 00c699d1a6 Update README.md 2025-04-29 23:33:28 +08:00
53 changed files with 11502 additions and 883 deletions
+67 -13
View File
@@ -12,23 +12,34 @@
## 程序有哪些功能?运行逻辑是什么?怎样使用? ## 程序有哪些功能?运行逻辑是什么?怎样使用?
### Boss炸弹 ### Boss炸弹
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。 扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ebf5c51c-f52c-4426-a60a-97916f663c6a" /> <img width="1280" height="2474" alt="Snipaste_2025-07-15_13-05-35" src="https://github.com/user-attachments/assets/30308ec2-1e3b-4a8f-b3fe-7d205394578d" />
#### 匹配步骤 #### 匹配步骤
1. 按照公司名称查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情 1. 选择职位来源、职位筛选条件
2. 检查Boss活跃度 <img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
- 如果Boss活跃度为本月活跃或更往前的时间,则会把职位标记为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
3. 对职位名称、职位类型、职位描述进行匹配 2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
- 如果匹配则自动点击开聊按钮 3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、Boss活跃度进行匹配
- 不匹配则标记这个职位为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位 - 如果可以匹配,则自动点击开聊按钮
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
- 首先查看是否命中策略为“在Boss直聘上标记不合适”的配置,如果命中则在Boss直聘上点击相关UI,标记不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位(对于“推荐列表中的职位”、“通过搜索找到的职位”,此项会回落到“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”)
- 然后查看是否命中策略为“在本地数据库中标记不合适,且7天内再遇到这个职位时直接跳过”的配置,如果命中则仅在本地数据库中记录,且本次运行将无视这个职位
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
#### 异常情况 #### 异常情况
- 当前页面筛选条件下,如果没有更多职位,则自动切换备选筛选条件,以获取更多新职位 - 当前页面筛选条件下,如果没有更多可投递职位,则自动切换筛选条件,以获取更多新职位;如果所有筛选条件搜索完没有更多可投递职位,则切换其它职位来源继续查找更多新职位;如果所有职位来源筛选完没有更多可投递职位,则等候一段时间后,会回到第一个职位来源的第一个筛选条件,从头开始查找职位
- 如当天开聊次数用完,本程序会暂停运行60分钟,之后尝试继续重新运行;如重新运行时间已在第二天,则将会继续开聊 - 如当天开聊次数用完,本程序会暂停运行60分钟,之后尝试继续重新运行;如重新运行时间已在第二天,则将会继续开聊
#### 运行效果
> Boss直聘界面已改版,如下是旧版Boss直聘界面
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
### 已读不回提醒器 ### 已读不回提醒器
Boss不明原因已读不回?简历就是投不出去? Boss不明原因已读不回?简历就是投不出去?
@@ -73,7 +84,7 @@ Boss不明原因已读不回?简历就是投不出去?
### 配置模板选项 ### 配置模板选项
初次见面,不会配置本程序?当心配置出现问题?本程序为一些关键的功能配置(例如:Boss炸弹中的期望公司设置、职位详情筛选设置;大语言模型设置)加入了配置模板,让本程序更易用,助你轻松完成配置,开始求职。不妨来试试吧 初次见面,不会配置本程序?当心配置出现问题?本程序为一些关键的功能配置(例如:Boss炸弹中的期望公司设置、职位详情筛选设置;大语言模型设置)加入了配置模板,让本程序更易用,助你轻松完成配置,开始求职。不妨来试试吧
<img width="576" alt="image" src="https://github.com/user-attachments/assets/2cccfece-17e7-4cc2-bf33-67f0ce500625" /> <img width="576" alt="image" src="https://github.com/user-attachments/assets/2cccfece-17e7-4cc2-bf33-67f0ce500625" />
@@ -108,6 +119,19 @@ Boss不明原因已读不回?简历就是投不出去?
``` ```
1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置 1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置
## 关于软件不能正常启动、不能按预期运行的一些说明
本程序一直都在不断地更新迭代中,包括新功能的增加以及Bug的修复。
然而作者本人只有 1台 macOS Apple Silicon)设备、1台 Windows 设备,且只有作者1人自测(白天工作,晚上熬夜开发、测试),测试条件不是太好(人力不足、设备不足),测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时,即认为可以准出、发版。
所以不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看,问题也会尝试进行修复。
如果部分朋友超级喜欢写代码,或者本身工作就是写代码,可以帮忙下载 [Debugtron](https://github.com/pd4d10/debugtron/releases/tag/v1.0.0-alpha.0) 来调试本程序,查看反馈本程序的日志(如下图)。
![image](https://github.com/user-attachments/assets/4ee9955c-e1c0-4399-9d00-ce89f6657bba)
在此先谢过了~
## 为什么要写这个程序? ## 为什么要写这个程序?
### 个人而言 ### 个人而言
以我为例,我真的求职求累了 以我为例,我真的求职求累了
@@ -133,8 +157,8 @@ Boss不明原因已读不回?简历就是投不出去?
一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场 一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场
当然,如果运气好: 当然,如果运气好:
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个 Offer - 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个不满意的 Offer
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”) - 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了不满意的 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧 为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧
@@ -147,7 +171,27 @@ Boss不明原因已读不回?简历就是投不出去?
人工筛选这些职位会有很大的心智负担,真的会吐…… 人工筛选这些职位会有很大的心智负担,真的会吐……
因此,我在Boss炸弹代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位 因此,我在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 +200,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>
感谢支持
@@ -90,7 +90,7 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
//#region get count of combinations //#region get count of combinations
// 计算符合限制条件的组合数量 // 计算符合限制条件的组合数量
export function calculateTotalCombinations(selectedFilters) { export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) {
const { const {
salaryList = [], salaryList = [],
experienceList = [], experienceList = [],
@@ -106,8 +106,21 @@ export function calculateTotalCombinations(selectedFilters) {
const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多 const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个 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 return accu * cur.length
}, 1) }, 1)
if (!includeEmptyCondition) {
result -= 1
}
return result
} }
//#endregion //#endregion
export function checkAnyCombineBossRecommendFilterHasCondition(value) {
if (!Object.keys(value ?? {}).length) {
return false
}
return Object.keys(value).some((k) => {
return !!value[k]?.length
})
}
@@ -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`
@@ -6,13 +6,28 @@
"scaleList": [], "scaleList": [],
"industryList": [] "industryList": []
}, },
"isSkipEmptyConditionForCombineRecommendJobFilter": false,
"expectJobRegExpStr": "", "expectJobRegExpStr": "",
"jobNotMatchStrategy": 1,
"jobNotActiveStrategy": 1,
"markAsNotActiveSelectedTimeRange": 7,
"expectCityList": [],
"expectCityNotMatchStrategy": 3,
"strategyScopeOptionWhenMarkJobCityNotMatch": 2,
"jobDetailRegExpMatchLogic": 1,
"autoReminder": { "autoReminder": {
"throttleIntervalMinutes": 10, "throttleIntervalMinutes": 10,
"rechatLimitDay": 21, "rechatLimitDay": 21,
"geminiApiKey": "", "geminiApiKey": "",
"rechatContentSource": 1, "rechatContentSource": 1,
"recentMessageQuantityForLlm": 8, "recentMessageQuantityForLlm": 8,
"rechatLlmFallback": 1 "rechatLlmFallback": 1,
} "onlyRemindBossWithExpectJobType": true
},
"jobSourceList": [
{
"type": "expect",
"enabled": true
}
]
} }
@@ -1,4 +1,4 @@
[ [
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐", "青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","巨量引擎","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","京东","沃东天骏","达达","达冠","百度","昆仑芯","小度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","智图","长地万方","瑞图万方","道道通","小熊博望","理想","蔚来","顺丰","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","淘宝","淘麦郎","天猫","盒马","口碑","优视","夸克","UC","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","得物","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","首汽租车","GoFun","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手","Grab","爱回收","洋钱罐","瓴岳","得到","思维造物","地平线","咪咕","翼支付","电信","天翼","联通","蓝湖","墨刀" "抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","巨量引擎","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","小鹅通","富途","京东","沃东天骏","达达","达冠","百度","昆仑芯","小度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","智图","长地万方","瑞图万方","道道通","小熊博望","理想","蔚来","顺丰","丰巢","中通","圆通","申通","跨越","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","淘宝","淘麦郎","天猫","盒马","口碑","优视","夸克","UC","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","51talk","爱学习","高思","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","从鲸","TEMU","得物","有赞","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","虾皮","首汽租车","GoFun","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","锐捷","Tenda","腾达","斐讯","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手","Grab","爱回收","洋钱罐","瓴岳","得到","思维造物","地平线","咪咕","翼支付","电信","天翼","联通","蓝湖","墨刀","海尔","美的","米哈游","传音","同花顺","国美","TCL"
] ]
File diff suppressed because it is too large Load Diff
@@ -17,7 +17,6 @@ import {
runtimeFolderPath, runtimeFolderPath,
ensureEditThisCookie, ensureEditThisCookie,
editThisCookieExtensionPath, editThisCookieExtensionPath,
isRunFromUi,
} from './utils.mjs' } from './utils.mjs'
import { EventEmitter } from 'node:events' import { EventEmitter } from 'node:events'
@@ -56,10 +55,8 @@ export async function main() {
page.once('close', async () => { page.once('close', async () => {
browser.close() browser.close()
if (isRunFromUi) { const electron = await import('electron')
const electron = await import('electron') electron.app.quit()
electron.app.quit()
}
}) })
const { dispose: disposeNavigationLock } = await blockNavigation(page, (req) => !req.url().startsWith('https://www.zhipin.com')) 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 extractZip from 'extract-zip'
import packageJson from './package.json' assert {type: 'json'} 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' const isUiDev = process.env.NODE_ENV === 'development'
export const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun') export const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
@@ -28,12 +27,8 @@ async function getEditThisCookieZipPath () {
if (editThisCookieZipPath) { if (editThisCookieZipPath) {
return editThisCookieZipPath return editThisCookieZipPath
} }
if (isRunFromUi) { const { app } = await import('electron')
const { app } = await import('electron') editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
} else {
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
}
return editThisCookieZipPath return editThisCookieZipPath
} }
@@ -51,11 +51,13 @@ const main = async () => {
pageLoaded: new SyncHook(), pageLoaded: new SyncHook(),
cookieWillSet: new SyncHook(['cookies']), cookieWillSet: new SyncHook(['cookies']),
userInfoResponse: new AsyncSeriesHook(['userInfo']), userInfoResponse: new AsyncSeriesHook(['userInfo']),
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']), newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']),
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']), newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
noPositionFoundForCurrentJob: new SyncHook(), noPositionFoundForCurrentJob: new SyncHook(),
noPositionFoundAfterTraverseAllJob: new SyncHook(), noPositionFoundAfterTraverseAllJob: new SyncHook(),
errorEncounter: new SyncHook(['errorInfo']) errorEncounter: new SyncHook(['errorInfo']),
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args'])
} }
initPlugins(hooks) initPlugins(hooks)
await hooks.daemonInitialized.callAsync() await hooks.daemonInitialized.callAsync()
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@geekgeekrun/sqlite-plugin", "name": "@geekgeekrun/sqlite-plugin",
"version": "0.0.1", "version": "0.0.2",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"dependencies": { "dependencies": {
@@ -1,3 +1,4 @@
import { JobSource } from "../enums";
import { requireTypeorm } from "../utils/module-loader"; import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm() const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
@@ -29,4 +30,9 @@ export class ChatStartupLog {
nullable: true nullable: true
}) })
autoStartupChatRecordId?: number; autoStartupChatRecordId?: number;
@Column({
nullable: true
})
jobSource?: JobSource;
} }
@@ -1,4 +1,4 @@
import { MarkAsNotSuitReason } from "../enums"; import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
import { requireTypeorm } from "../utils/module-loader"; import { requireTypeorm } from "../utils/module-loader";
import { ChatStartupFrom } from "./ChatStartupLog"; import { ChatStartupFrom } from "./ChatStartupLog";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm() const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
@@ -27,6 +27,11 @@ export class MarkAsNotSuitLog {
}) })
markReason?: MarkAsNotSuitReason markReason?: MarkAsNotSuitReason
@Column({
nullable: true
})
markOp?: MarkAsNotSuitOp
@Column({ @Column({
nullable: true nullable: true
}) })
@@ -36,4 +41,9 @@ export class MarkAsNotSuitLog {
nullable: true nullable: true
}) })
autoStartupChatRecordId?: number; autoStartupChatRecordId?: number;
@Column({
nullable: true
})
jobSource?: JobSource;
} }
+31 -1
View File
@@ -3,4 +3,34 @@ export enum MarkAsNotSuitReason {
BOSS_INACTIVE = 1, BOSS_INACTIVE = 1,
USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON = 2, USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON = 2,
JOB_NOT_SUIT = 3, JOB_NOT_SUIT = 3,
} JOB_CITY_NOT_SUIT = 4,
JOB_WORK_EXP_NOT_SUIT = 5,
JOB_SALARY_NOT_SUIT = 6,
}
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,
}
+44 -5
View File
@@ -1,4 +1,4 @@
import { DataSource } from "typeorm"; import { DataSource, Raw } from "typeorm";
import { BossActiveStatusRecord } from "./entity/BossActiveStatusRecord"; import { BossActiveStatusRecord } from "./entity/BossActiveStatusRecord";
import { BossInfo } from "./entity/BossInfo"; import { BossInfo } from "./entity/BossInfo";
import { CompanyInfo } from "./entity/CompanyInfo"; import { CompanyInfo } from "./entity/CompanyInfo";
@@ -245,7 +245,7 @@ export async function saveChatStartupRecord(
ds: DataSource, ds: DataSource,
_jobInfo, _jobInfo,
{ encryptUserId }, { encryptUserId },
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined } = {} { autoStartupChatRecordId = undefined, chatStartupFrom = undefined, jobSource = undefined } = {}
) { ) {
const { jobInfo } = _jobInfo; const { jobInfo } = _jobInfo;
@@ -256,7 +256,8 @@ export async function saveChatStartupRecord(
encryptCurrentUserId: encryptUserId, encryptCurrentUserId: encryptUserId,
encryptJobId: jobInfo.encryptId, encryptJobId: jobInfo.encryptId,
autoStartupChatRecordId, autoStartupChatRecordId,
chatStartupFrom chatStartupFrom,
jobSource,
} }
Object.assign(chatStartupLog, chatStartupLogPayload) Object.assign(chatStartupLog, chatStartupLogPayload)
@@ -270,7 +271,7 @@ export async function saveMarkAsNotSuitRecord(
ds: DataSource, ds: DataSource,
_jobInfo, _jobInfo,
{ encryptUserId }, { encryptUserId },
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined } = {} { autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined, jobSource = undefined } = {}
) { ) {
const { jobInfo } = _jobInfo; const { jobInfo } = _jobInfo;
@@ -283,7 +284,9 @@ export async function saveMarkAsNotSuitRecord(
autoStartupChatRecordId, autoStartupChatRecordId,
markFrom, markFrom,
markReason, markReason,
extInfo: extInfo ? JSON.stringify(extInfo) : undefined extInfo: extInfo ? JSON.stringify(extInfo) : undefined,
markOp,
jobSource,
} }
Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload) Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload)
@@ -326,3 +329,39 @@ export async function saveGptCompletionRequestRecord(
//#endregion //#endregion
return 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
}
+103 -6
View File
@@ -22,16 +22,28 @@ import { ChatMessageRecord } from './entity/ChatMessageRecord'
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord' import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
import sqlite3 from 'sqlite3'; import sqlite3 from 'sqlite3';
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord } from "./handlers"; import {
saveChatStartupRecord,
saveJobInfoFromRecommendPage,
saveMarkAsNotSuitRecord,
getNotSuitMarkRecordsInLastSomeDays,
getChatStartupRecordsInLastSomeDays,
getBossIdsByJobIds
} from "./handlers";
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable"; import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
import minimist from 'minimist' import minimist from 'minimist'
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable"; import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
import { Init1000000000000 } from "./migrations/1000000000000-Init";
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
const lodashImportPromise = import('lodash-es')
export function initDb(dbFilePath) { export function initDb(dbFilePath) {
const { DataSource } = requireTypeorm() const { DataSource } = requireTypeorm()
const appDataSource = new DataSource({ const appDataSource = new DataSource({
type: "sqlite", type: "sqlite",
synchronize: true, synchronize: false,
logging: true, logging: true,
logger: "simple-console", logger: "simple-console",
database: dbFilePath, database: dbFilePath,
@@ -57,8 +69,11 @@ export function initDb(dbFilePath) {
LlmModelUsageRecord, LlmModelUsageRecord,
], ],
migrations: [ migrations: [
Init1000000000000,
UpdateChatStartupLogTable1729182577167, UpdateChatStartupLogTable1729182577167,
UpdateBossInfoTable1732032381304, UpdateBossInfoTable1732032381304,
AddColumnForMarkAsNotSuitLog1746092370665,
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526
], ],
migrationsRun: true migrationsRun: true
}); });
@@ -97,27 +112,109 @@ export default class SqlitePlugin {
return await userInfoRepository.save(user); 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 { 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)
}
}
}
);
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async (_jobInfo) => { hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async (_jobInfo) => {
const ds = await this.initPromise; const ds = await this.initPromise;
await saveJobInfoFromRecommendPage(ds, _jobInfo); await saveJobInfoFromRecommendPage(ds, _jobInfo);
}); });
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList } = {}) => { hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList, jobSource = undefined } = {}) => {
const ds = await this.initPromise; const ds = await this.initPromise;
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, { return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId, autoStartupChatRecordId: this.runRecordId,
chatStartupFrom chatStartupFrom,
jobSource
}); });
}); });
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined } = {}) => { hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined, jobSource = undefined } = {}) => {
const ds = await this.initPromise; const ds = await this.initPromise;
return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, { return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId, autoStartupChatRecordId: this.runRecordId,
markFrom, markFrom,
markReason, markReason,
extInfo extInfo,
markOp,
jobSource
}); });
}); });
} }
@@ -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 = [ const ViewEntities = [
"v_boss_library", VBossLibrary,
"v_chat_startup_log", VChatStartupLog,
"v_company_library", VCompanyLibrary,
"v_job_library", VJobLibrary,
"v_mark_as_not_suit_log" VMarkAsNotSuitLog,
]; ]
export class UpdateChatStartupLogTable1729182577167 export class UpdateChatStartupLogTable1729182577167 implements MigrationInterface {
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
for (const viewName of viewNames) { for (const EntityDefinition of ViewEntities) {
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`); 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.hasTable("boss_active_status_record")) {
if (await queryRunner.hasColumn("boss_active_status_record", "updateDate")) { 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> { 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 = [ const ViewEntities = [
"v_boss_library", VBossLibrary,
"v_chat_startup_log", VChatStartupLog,
"v_company_library", VCompanyLibrary,
"v_job_library", VJobLibrary,
"v_mark_as_not_suit_log", VMarkAsNotSuitLog,
]; ]
export class UpdateBossInfoTable1732032381304 implements MigrationInterface { export class UpdateBossInfoTable1732032381304 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
for (const viewName of viewNames) { for (const EntityDefinition of ViewEntities) {
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`); 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.hasTable("boss_info")) {
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) { 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> {} 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> {
}
}
@@ -1,7 +1,3 @@
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 () { export function requireTypeorm () {
const importResult = require('typeorm') const importResult = require('typeorm')
return importResult return importResult
+14 -8
View File
@@ -3,7 +3,7 @@ import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite' import UnoCSS from 'unocss/vite'
import { presetUno, presetAttributify, presetIcons } from 'unocss' import { presetUno, presetAttributify, presetIcons } from 'unocss'
import transformerDirective from "@unocss/transformer-directives"; import transformerDirective from '@unocss/transformer-directives'
import Replace from 'unplugin-replace/vite' import Replace from 'unplugin-replace/vite'
process.env = { ...process.env, ...loadEnv(process.env.NODE_ENV!, process.cwd()) } process.env = { ...process.env, ...loadEnv(process.env.NODE_ENV!, process.cwd()) }
@@ -13,11 +13,17 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
external: [] external: []
}, },
minify: 'terser' minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
}, },
plugins: [ plugins: [
externalizeDepsPlugin({ 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({ Replace({
delimiters: ['', ''], delimiters: ['', ''],
@@ -26,11 +32,11 @@ export default defineConfig({
values: [ values: [
{ {
find: /<measurement_id>/g, 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, 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: { preload: {
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
build: { build: {
minify: 'terser' minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
} }
}, },
renderer: { renderer: {
@@ -52,11 +58,11 @@ export default defineConfig({
vue(), vue(),
UnoCSS({ UnoCSS({
presets: [presetUno(), presetAttributify(), presetIcons()], presets: [presetUno(), presetAttributify(), presetIcons()],
transformers: [transformerDirective()], transformers: [transformerDirective()]
}) })
], ],
build: { build: {
minify: 'terser' minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
} }
} }
}) })
+6 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "geekgeekrun-ui", "name": "geekgeekrun-ui",
"version": "0.4.0", "version": "0.9.4",
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!", "description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "geekgeekrun", "author": "geekgeekrun",
@@ -38,12 +38,15 @@
"JSONStream": "^1.3.5", "JSONStream": "^1.3.5",
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "^6.1.7", "electron-updater": "^6.1.7",
"lodash-es": "^4.17.21",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"mitt": "^3.0.1",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
"pinia": "^3.0.2", "pinia": "^3.0.2",
"puppeteer": "20.1.0", "puppeteer": "20.1.0",
"puppeteer-extra-plugin-stealth": "2.11.2", "puppeteer-extra-plugin-stealth": "2.11.2",
"uuid": "^11.1.0" "uuid": "^11.1.0",
"vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2", "@electron-toolkit/eslint-config": "^1.0.2",
@@ -65,12 +68,11 @@
"electron": "28.2.0", "electron": "28.2.0",
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"electron-vite": "^2.0.0", "electron-vite": "^2.0.0",
"element-plus": "^2.5.5", "element-plus": "^2.8.4",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.1", "eslint-plugin-vue": "^9.20.1",
"find-chrome-bin": "^2.0.1", "find-chrome-bin": "^2.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"sass": "^1.70.0", "sass": "^1.70.0",
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"version": "0.4.0", "version": "0.9.4",
"buildVersion": 9, "buildVersion": 20,
"buildTime": 1745903517984, "buildTime": 1760377927509,
"buildHash": "25e52530228e78c3cf78caa053c7da6bb138e5e2", "buildHash": "51ca5dd71d62de2965fb53cff9d03c0d3552eabb",
"name": "geekgeekrun-ui" "name": "geekgeekrun-ui"
} }
File diff suppressed because it is too large Load Diff
@@ -33,6 +33,13 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
await removeLastUsedAndAvailableBrowserPath() await removeLastUsedAndAvailableBrowserPath()
return null return null
} }
// blacklist browser
if (path.includes(`Microsoft\\Edge\\Application\\msedge.exe`)) {
await removeLastUsedAndAvailableBrowserPath()
return null
}
return { return {
executablePath: path, executablePath: path,
browser browser
@@ -8,12 +8,10 @@ import {
getLastUsedAndAvailableBrowser, getLastUsedAndAvailableBrowser,
removeLastUsedAndAvailableBrowserPath removeLastUsedAndAvailableBrowserPath
} from '../browser-history' } from '../browser-history'
import { getExecutableFileVersion } from '@geekgeekrun/utils/windows-only/file.mjs'
import gtag from '../../../../utils/gtag' import gtag from '../../../../utils/gtag'
const getPuppeteerManagerModule = async () => { const getPuppeteerManagerModule = async () => {
let puppeteerManager const puppeteerManager = await import('@puppeteer/browsers')
puppeteerManager = await import('@puppeteer/browsers')
return puppeteerManager return puppeteerManager
} }
@@ -76,7 +74,8 @@ export const checkAndDownloadPuppeteerExecutable = async (
browser: puppeteerManager.Browser.CHROME, browser: puppeteerManager.Browser.CHROME,
cacheDir, cacheDir,
buildId: EXPECT_CHROMIUM_BUILD_ID, buildId: EXPECT_CHROMIUM_BUILD_ID,
downloadProgressCallback: options.downloadProgressCallback downloadProgressCallback: options.downloadProgressCallback,
baseUrl: `https://registry.npmmirror.com/-/binary/chrome-for-testing`
}) })
} else { } else {
gtag('use_installed_browser') gtag('use_installed_browser')
@@ -128,33 +127,8 @@ export const getAnyAvailablePuppeteerExecutable = async (): Promise<BrowserInfo
export async function findAndLocateUserInstalledChromiumExecutableSync(): Promise<BrowserInfo> { export async function findAndLocateUserInstalledChromiumExecutableSync(): Promise<BrowserInfo> {
const exceptChromiumMainVersion = Number(EXPECT_CHROMIUM_BUILD_ID.split('.')[0]) const exceptChromiumMainVersion = Number(EXPECT_CHROMIUM_BUILD_ID.split('.')[0])
// For windows, try to find Edge(chromium) const findChrome: typeof import('find-chrome-bin').findChrome = (await import('find-chrome-bin'))
if (os.platform() === 'win32') { .findChrome
// 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 targetBrowser = await findChrome({ const targetBrowser = await findChrome({
min: exceptChromiumMainVersion min: exceptChromiumMainVersion
}) })
@@ -25,14 +25,10 @@ function runWithDaemon({ runRecordId, runMode, parentProcessPipe }) {
const subProcessOfCore = childProcess.spawn( const subProcessOfCore = childProcess.spawn(
process.argv[0], process.argv[0],
isUiDev isUiDev
? [...process.argv.slice(1), `--run-record-id=${runRecordId}`] ? [process.argv[1], `--run-record-id=${runRecordId}`, `--mode=${runMode}`]
: [`--run-record-id=${runRecordId}`], : [`--run-record-id=${runRecordId}`, `--mode=${runMode}`],
{ {
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'], stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
env: {
...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: runMode
}
} }
) )
@@ -86,13 +86,15 @@ const runAutoChat = async () => {
pageLoaded: new SyncHook(), pageLoaded: new SyncHook(),
cookieWillSet: new SyncHook(['cookies']), cookieWillSet: new SyncHook(['cookies']),
userInfoResponse: new AsyncSeriesHook(['userInfo']), userInfoResponse: new AsyncSeriesHook(['userInfo']),
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']), jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']),
newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']), newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']),
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']), newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
jobMarkedAsNotSuit: new AsyncSeriesHook(['positionInfoDetail', 'markDetail']), jobMarkedAsNotSuit: new AsyncSeriesHook(['positionInfoDetail', 'markDetail']),
noPositionFoundForCurrentJob: new SyncHook(), noPositionFoundForCurrentJob: new SyncHook(),
noPositionFoundAfterTraverseAllJob: new SyncHook(), noPositionFoundAfterTraverseAllJob: new SyncHook(),
errorEncounter: new SyncHook(['errorInfo']) errorEncounter: new SyncHook(['errorInfo']),
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args'])
} }
initPlugins(hooks) initPlugins(hooks)
@@ -4,6 +4,10 @@ import {
readStorageFile, readStorageFile,
writeStorageFile writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import {
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 { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
import { import {
saveJobInfoFromRecommendPage, saveJobInfoFromRecommendPage,
@@ -13,7 +17,7 @@ import {
} from '@geekgeekrun/sqlite-plugin/dist/handlers' } from '@geekgeekrun/sqlite-plugin/dist/handlers'
import { initDb } from '@geekgeekrun/sqlite-plugin' import { initDb } from '@geekgeekrun/sqlite-plugin'
import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' 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 } from '@geekgeekrun/sqlite-plugin/dist/enums'
import fs from 'node:fs' import fs from 'node:fs'
import { Target } from 'puppeteer' import { Target } from 'puppeteer'
@@ -38,6 +42,49 @@ const attachRequestsListener = async (target: Target) => {
if (!page) { if (!page) {
return return
} }
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) => { page.on('response', async (response) => {
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) { if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
@@ -81,7 +128,7 @@ const attachRequestsListener = async (target: Target) => {
const reasonCodeToTextMap = await readStorageFile( const reasonCodeToTextMap = await readStorageFile(
'job-not-suit-reason-code-to-text-cache.json' 'job-not-suit-reason-code-to-text-cache.json'
) )
const jobSource = await getCurrentJobSource()
const markDetail = { const markDetail = {
markFrom: ChatStartupFrom.ManuallyFromRecommendList, markFrom: ChatStartupFrom.ManuallyFromRecommendList,
extInfo: { extInfo: {
@@ -90,9 +137,15 @@ const attachRequestsListener = async (target: Target) => {
text: reasonCodeToTextMap[chosenCode] 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('活跃度低')) { if (reasonCodeToTextMap[chosenCode]?.includes('活跃度低')) {
markDetail.markReason = MarkAsNotSuitReason.BOSS_INACTIVE markDetail.markReason = MarkAsNotSuitReason.BOSS_INACTIVE
markDetail.extInfo.bossActiveTimeDesc = currentJobData?.bossInfo.activeTimeDesc markDetail.extInfo.bossActiveTimeDesc = currentJobData?.bossInfo.activeTimeDesc
@@ -127,7 +180,12 @@ const attachRequestsListener = async (target: Target) => {
const currentUserInfo = await page.evaluate( const currentUserInfo = await page.evaluate(
'document.querySelector(".job-detail-box").__vue__.$store.state.userInfo' '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 saveChatStartupRecord(
await dbInitPromise, await dbInitPromise,
currentJobData, currentJobData,
@@ -135,7 +193,8 @@ const attachRequestsListener = async (target: Target) => {
encryptUserId: currentUserInfo.encryptUserId encryptUserId: currentUserInfo.encryptUserId
}, },
{ {
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
jobSource: JobSource[jobSource]
} }
) )
} else if ( } else if (
@@ -95,9 +95,71 @@ export default function initIpc() {
if (hasOwn(payload, 'expectJobDescRegExpStr')) { if (hasOwn(payload, 'expectJobDescRegExpStr')) {
bossConfig.expectJobDescRegExpStr = 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')) { if (hasOwn(payload, 'autoReminder')) {
bossConfig.autoReminder = 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
}
promiseArr.push(writeConfigFile('boss.json', bossConfig)) promiseArr.push(writeConfigFile('boss.json', bossConfig))
if (hasOwn(payload, 'expectCompanies')) { if (hasOwn(payload, 'expectCompanies')) {
@@ -142,12 +204,15 @@ export default function initIpc() {
} }
const subProcessEnv = { const subProcessEnv = {
...process.env, ...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'geekAutoStartWithBossDaemon',
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
} }
subProcessOfPuppeteer = childProcess.spawn( subProcessOfPuppeteer = childProcess.spawn(
process.argv[0], process.argv[0],
[...process.argv.slice(1), '--mode-to-daemon=geekAutoStartWithBossMain'], [
process.argv[1],
`--mode=geekAutoStartWithBossDaemon`,
`--mode-to-daemon=geekAutoStartWithBossMain`
],
{ {
env: subProcessEnv, env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'] stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
@@ -196,13 +261,16 @@ export default function initIpc() {
} }
const subProcessEnv = { const subProcessEnv = {
...process.env, ...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'readNoReplyAutoReminder',
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
} }
subProcessOfPuppeteer = childProcess.spawn(process.argv[0], process.argv.slice(1), { subProcessOfPuppeteer = childProcess.spawn(
env: subProcessEnv, process.argv[0],
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'] [process.argv[1], `--mode=readNoReplyAutoReminder`],
}) {
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
}
)
// console.log(subProcessOfPuppeteer) // console.log(subProcessOfPuppeteer)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
subProcessOfPuppeteer!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => { subProcessOfPuppeteer!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => {
@@ -255,15 +323,10 @@ export default function initIpc() {
if (subProcessOfCheckAndDownloadDependencies) { if (subProcessOfCheckAndDownloadDependencies) {
return return
} }
const subProcessEnv = {
...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'checkAndDownloadDependenciesForInit'
}
subProcessOfCheckAndDownloadDependencies = childProcess.spawn( subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
process.argv[0], process.argv[0],
process.argv.slice(1), [process.argv[1], `--mode=checkAndDownloadDependenciesForInit`],
{ {
env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc'] stdio: [null, null, null, 'pipe', 'ipc']
} }
) )
@@ -325,12 +388,11 @@ export default function initIpc() {
} }
const subProcessEnv = { const subProcessEnv = {
...process.env, ...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'launchBossZhipinLoginPageWithPreloadExtension',
PUPPETEER_EXECUTABLE_PATH: (await getAnyAvailablePuppeteerExecutable())!.executablePath PUPPETEER_EXECUTABLE_PATH: (await getAnyAvailablePuppeteerExecutable())!.executablePath
} }
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn( subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
process.argv[0], process.argv[0],
process.argv.slice(1), [process.argv[1], `--mode=launchBossZhipinLoginPageWithPreloadExtension`],
{ {
env: subProcessEnv, env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc'] stdio: [null, null, null, 'pipe', 'ipc']
@@ -406,13 +468,16 @@ export default function initIpc() {
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable() const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
const subProcessEnv = { const subProcessEnv = {
...process.env, ...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'launchBossSite',
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable!.executablePath PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable!.executablePath
} }
subProcessOfOpenBossSite = childProcess.spawn(process.argv[0], process.argv.slice(1), { subProcessOfOpenBossSite = childProcess.spawn(
env: subProcessEnv, process.argv[0],
stdio: ['inherit', 'inherit', 'inherit', 'pipe'] [process.argv[1], `--mode=launchBossSite`],
}) {
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe']
}
)
subProcessOfOpenBossSite.once('exit', () => { subProcessOfOpenBossSite.once('exit', () => {
subProcessOfOpenBossSiteDefer = null subProcessOfOpenBossSiteDefer = null
}) })
@@ -501,16 +566,15 @@ export default function initIpc() {
ipcMain.handle('save-resume-content', saveResumeHandler) ipcMain.handle('save-resume-content', saveResumeHandler)
resumeEditorWindow?.once('closed', () => { resumeEditorWindow?.once('closed', () => {
ipcMain.removeHandler('save-resume-content') ipcMain.removeHandler('save-resume-content')
ipcMain.removeHandler('fetch-resume-content')
defer.reject(new Error('cancel')) 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 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 () => { ipcMain.on('no-reply-reminder-prompt-edit', async () => {
const template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false }) const template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false })
if (!template) { if (!template) {
@@ -32,6 +32,10 @@ const rechatLlmFallback =
readConfigFile('boss.json').autoReminder?.rechatLlmFallback ?? readConfigFile('boss.json').autoReminder?.rechatLlmFallback ??
RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
const expectJobTypeRegExpStr = readConfigFile('boss.json').expectJobTypeRegExpStr
const onlyRemindBossWithExpectJobType =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithExpectJobType ?? !!expectJobTypeRegExpStr
const dbInitPromise = initDb(getPublicDbFilePath()) const dbInitPromise = initDb(getPublicDbFilePath())
export const pageMapByName: { export const pageMapByName: {
@@ -117,7 +121,6 @@ const mainLoop = async () => {
'你与该职位竞争者PK情况', '你与该职位竞争者PK情况',
'简历诊断提醒', '简历诊断提醒',
'附件简历还没准备好', '附件简历还没准备好',
'开场问题,期待你的回答',
'设置合适的期望薪资范围' '设置合适的期望薪资范围'
].map((it) => new RegExp(it)) ].map((it) => new RegExp(it))
browser = await bootstrap() browser = await bootstrap()
@@ -198,9 +201,10 @@ const mainLoop = async () => {
(rechatLimitDay && it.updateTime (rechatLimitDay && it.updateTime
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000 ? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
: true) && : true) &&
((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) || ((((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) ||
canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) && canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) &&
!it.unreadCount !it.unreadCount) ||
(!it.lastIsSelf && it.lastText === '开场问题,期待你的回答'))
) )
}) })
@@ -267,6 +271,24 @@ const mainLoop = async () => {
}) })
} }
await sleepWithRandomDelay(1500) 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( const conversationInfo = await pageMapByName.boss?.evaluate(
`document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$` `document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$`
) )
@@ -280,6 +302,7 @@ const mainLoop = async () => {
const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0 const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0
if ( if (
isExpectJobTypeMatch &&
historyMessageList[historyMessageList.length - 1].isSelf && historyMessageList[historyMessageList.length - 1].isSelf &&
historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ && historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ &&
((conversationInfo && ((conversationInfo &&
+6 -1
View File
@@ -1,4 +1,9 @@
const runMode = process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE import minimist from 'minimist'
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 () => { ;(async () => {
switch (runMode) { switch (runMode) {
+22 -5
View File
@@ -2,17 +2,34 @@ import gtag from '.'
export default class GtagPlugin { export default class GtagPlugin {
apply(hooks) { apply(hooks) {
hooks.newChatStartup.tap('GtagPlugin', (_, { chatStartupFrom }) => { hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom, jobSource }) => {
gtag('new_chat_startup', { chatStartupFrom }) gtag('new_chat_startup', {
}) chatStartupFrom,
hooks.jobMarkedAsNotSuit.tap('GtagPlugin', (_, { markFrom }) => { encryptJobId: jobData.jobInfo.encryptId,
gtag('job_marked_as_not_suit', { markFrom }) 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', () => { hooks.noPositionFoundForCurrentJob.tap('GtagPlugin', () => {
gtag('no_position_found_for_current_job') gtag('no_position_found_for_current_job')
}) })
hooks.noPositionFoundAfterTraverseAllJob.tap('GtagPlugin', () => { hooks.noPositionFoundAfterTraverseAllJob.tap('GtagPlugin', () => {
gtag('no_position_found_after_traverse_all_job') gtag('no_position_found_after_traverse_all_job')
}) })
hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => {
gtag('encounter_empty_rec_job_list', { pageQuery })
})
} }
} }
+2 -1
View File
@@ -8,8 +8,9 @@ export let mainWindow: BrowserWindow | null = null
export function createMainWindow(): BrowserWindow { export function createMainWindow(): BrowserWindow {
// Create the browser window. // Create the browser window.
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1024, width: 1280,
height: 720, height: 720,
minWidth: 1280,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
...(process.platform === 'linux' ...(process.platform === 'linux'
@@ -1,7 +1,7 @@
<template> <template>
<div class="job-combo-filter"> <div class="job-combo-filter">
<div class="filter-item"> <div class="filter-item">
<div>薪资待遇</div> <div font-size-12px>薪资待遇</div>
<el-select <el-select
v-model="modelValue.salaryList" v-model="modelValue.salaryList"
multiple multiple
@@ -18,7 +18,7 @@
</el-select> </el-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<div>工作经验</div> <div font-size-12px>工作经验</div>
<el-select <el-select
v-model="modelValue.experienceList" v-model="modelValue.experienceList"
multiple multiple
@@ -35,7 +35,7 @@
</el-select> </el-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<div>学历要求</div> <div font-size-12px>学历要求</div>
<el-select <el-select
v-model="modelValue.degreeList" v-model="modelValue.degreeList"
multiple multiple
@@ -52,7 +52,7 @@
</el-select> </el-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<div>公司行业</div> <div font-size-12px>公司行业</div>
<el-select <el-select
v-model="modelValue.industryList" v-model="modelValue.industryList"
multiple multiple
@@ -75,7 +75,7 @@
</el-select> </el-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<div>公司规模</div> <div font-size-12px>公司规模</div>
<el-select <el-select
v-model="modelValue.scaleList" v-model="modelValue.scaleList"
multiple multiple
@@ -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>
@@ -109,8 +109,14 @@ import { ElForm, ElMessage } from 'element-plus'
import { ref, onUnmounted, onMounted } from 'vue' import { ref, onUnmounted, onMounted } from 'vue'
import { checkCookieListFormat } from '../../../../common/utils/cookie' import { checkCookieListFormat } from '../../../../common/utils/cookie'
import { useRouter } from 'vue-router' 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 router = useRouter()
const cookieInvalid = ref(false) const cookieInvalid = ref(false)
@@ -1,5 +1,5 @@
<template> <template>
<div class="first-run-readme" ref="componentRootEl"> <div ref="componentRootEl" class="first-run-readme">
<div class="first-run-readme__inner"> <div class="first-run-readme__inner">
<div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div> <div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div>
<div class="readme-desc"> <div class="readme-desc">
@@ -12,25 +12,32 @@
> >
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']"> <ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
本程序从某种程度上说属于辅助工具<el-link 本程序从某种程度上说属于辅助工具<el-link
@click=" color-blue
electron.ipcRenderer.send( @click.stop.prevent="
'open-external-link', () => {
'https://about.zhipin.com/agreement/' electron.ipcRenderer.send(
) 'open-external-link',
'https://about.zhipin.com/agreement/'
)
gtagRenderer('view_boss_agreement_clicked')
}
" "
>Boss直聘用户协议</el-link >Boss直聘用户协议</el-link
>相关条款相违背 >2023年3月版相关条款相违背您在注册Boss直聘时已签署过这一条款根据该条款
根据该条款如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b <i>用户的平台使用义务</i><i>违约责任</i>
class="color-red" 章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录</b >账号被强制退出登录账号被限制使用账号被封禁</b
><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><b >且如果相关风险发生<b class="color-red">您需要自行承担相关后果本程序不负责</b>
class="color-red"
>本程序不负责</b
>
</ElCheckbox> </ElCheckbox>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']"> <ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊Boss以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b 本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b
class="color-red" class="color-red"
>注意节制</b >注意节制</b
@@ -38,39 +45,58 @@
>注册一个本程序专用的新的Boss直聘账号</b >注册一个本程序专用的新的Boss直聘账号</b
>进行求职 >进行求职
</ElCheckbox> </ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序原理是模拟用户在Boss直聘网页上进行点击操作Boss直聘网站每过一段时间会发生改版且有可能包含A/B实验这将导致本程序相关脚本失效此时您可以点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']"> <ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
您的雇主可能会对您的计算机终端或网络进行监控从而审计跟踪您的行为建议您<b 本程序原理是模拟用户在Boss直聘网页上寻找关键元素并进行点击操作Boss直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red" class="color-red"
>不要在您雇主提供的计算机终端或网络上使用本程序</b >浏览器重复闪退重新启动</b
> >如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox> </ElCheckbox>
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']"> <ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘<b 您所在公司可能会对您的计算机终端或网络进行<b color-red>监控</b>从而<b color-red
class="color-red" >审计跟踪</b
>不会泄露给第三方</b >您的行为上级 / HRBP 团队可能会从 IT
>也不会进行除自动开聊Boss以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b 团队处获取到监控数据从而了解团队成员离职倾向如果您不希望上级 / HRBP
团队了解到您正在求职建议您<b color-red
>不要在您所在公司提供的计算机终端或网络上使用本程序</b
> >
</ElCheckbox> </ElCheckbox>
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']"> <ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
本程序<b class="color-red" 本程序尊重您的隐私<b color-red
>不会上报您的隐私诸如能够识别出您身份的信息不会向您的雇主报告您的求职行为</b >不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及上级 / HRBP
>如果您在使用过程中遇上程序错误您可以点击程序左下角进行反馈 团队报告您的求职行为不会向猎头公司泄露您的信息</b
>
</ElCheckbox> </ElCheckbox>
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']"> <ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
<b class="color-red">本程序不对您的求职过程与结果负责</b 本程序<b class="color-red">没有内置任何付费功能</b><b class="color-red"
>为您开聊的职位均在Boss直聘上发布<b class="color-red">自行甄别为您开聊的公司</b >下载使用是免费的</b
><b class="color-red">认真决定是否参加面试</b><b class="color-red">慎重选择Offer</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>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']"> <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>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']"> <ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行如果您有顾虑建议通过VMWare 请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
WorkStation/FusionVirtualBoxHyper-V/Windows沙盒等虚拟化技术运行本程序如发生问题请点击程序左下角进行反馈 >如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃<b
class="color-red"
>在Boss直聘上屏蔽当前公司及与之关联的公司</b
>
</ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过
VMware Workstation / FusionOracle VirtualBoxMicrosoft Hyper-V
等虚拟化技术运行本程序如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox> </ElCheckbox>
</ElCheckboxGroup> </ElCheckboxGroup>
</article> </article>
@@ -86,26 +112,36 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus' import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus'
import { ref, onMounted, onBeforeMount } from 'vue'; import { ref, onMounted, onBeforeMount } from 'vue'
import { gtagRenderer } from '@renderer/utils/gtag' 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 electron = window.electron
const readmeItemCheckStatusList = ref<number[]>([]) const readmeItemCheckStatusList = ref<number[]>([])
const handleCancel = () => { const handleCancel = async () => {
gtagRenderer('cancel_clicked')
await sleep(500)
electron.ipcRenderer.invoke('exit-app-immediately') electron.ipcRenderer.invoke('exit-app-immediately')
} }
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({}) const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
const handleSubmit = () => { const handleSubmit = () => {
gtagRenderer('submit_clicked') gtagRenderer('submit_clicked')
const COUNT = 9 const COUNT = 10
if (readmeItemCheckStatusList.value.length !== COUNT) { if (readmeItemCheckStatusList.value.length !== COUNT) {
gtagRenderer('agreement_not_finish_read_tip_displayed') gtagRenderer('agreement_not_finish_read_tip_displayed')
ElMessage.warning( ElMessage.warning({
`您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧` message: `您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`,
) grouping: true
})
unreadItemsAfterClickSubmit.value = {} unreadItemsAfterClickSubmit.value = {}
for (let i = 0; i < COUNT; i++) { for (let i = 0; i < COUNT; i++) {
if (!readmeItemCheckStatusList.value.includes(i)) { if (!readmeItemCheckStatusList.value.includes(i)) {
@@ -127,13 +163,10 @@ const handleReadmeItemCheckStatusListChange = (value: number[]) => {
const componentRootEl = ref<HTMLElement>() const componentRootEl = ref<HTMLElement>()
onMounted(() => { onMounted(() => {
const ro = new ResizeObserver(() => { const ro = new ResizeObserver(() => {
electron.ipcRenderer.send( electron.ipcRenderer.send('update-window-size', {
'update-window-size', width: componentRootEl.value!.offsetWidth,
{ height: componentRootEl.value!.offsetHeight
width: componentRootEl.value!.offsetWidth, })
height: componentRootEl.value!.offsetHeight,
}
)
}) })
ro.observe(componentRootEl.value!) ro.observe(componentRootEl.value!)
onBeforeMount(() => { onBeforeMount(() => {
@@ -254,7 +254,7 @@ import {
} from 'element-plus' } from 'element-plus'
import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue' import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue'
import { ref, onMounted, watch, nextTick, computed } from '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 { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../../common/constant'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { sleep } from '@geekgeekrun/utils/sleep.mjs' import { sleep } from '@geekgeekrun/utils/sleep.mjs'
@@ -267,6 +267,13 @@ interface LlmConfigItem {
enabled: true enabled: true
} }
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'llm-config',
...params
})
}
function getNewConfigItem(): LlmConfigItem { function getNewConfigItem(): LlmConfigItem {
return { return {
id: uuid(), id: uuid(),
@@ -1,499 +0,0 @@
<template>
<div class="form-wrap geek-auto-start-run-with-boss">
<el-form ref="formRef" :model="formContent" label-position="top" :rules="formRules">
<el-form-item label="BOSS直聘 Cookie">
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
>编辑Cookie</el-button
>
</el-form-item>
<!-- <el-form-item
label="钉钉机器人 AccessToken(用于记录开聊,请勿使用公司内部群)"
prop="dingtalkRobotAccessToken"
>
<el-input v-model="formContent.dingtalkRobotAccessToken" />
</el-form-item> -->
<div>
<el-form-item mb0>
<div>
是否查看职位详情的条件
<span font-size-12px>以下条件为空表示不筛选</span>
</div>
</el-form-item>
<el-form-item prop="expectCompanies" mb10px>
<div
v-full
font-size-12px
flex
:style="{
justifyContent: 'space-between',
alignItems: 'center',
width: '100%'
}"
>
<div>
期望公司以逗号分隔不区分大小写<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_expect_company_figure')"
>
<template #content>
<img block h-270px src="./resources/intro-of-job-entry.png" />
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span>期望公司信息位置图示</el-button
>
</el-tooltip>
</div>
<el-dropdown @command="handleExpectCompanyTemplateClicked">
<el-button size="small"
>期望公司模板 <el-icon class="el-icon--right"><arrow-down /></el-icon
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in expectCompanyTemplateList"
:key="item.name"
:command="item"
>{{ item.name }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-input
v-model="formContent.expectCompanies"
:autosize="{ minRows: 4 }"
max-h-8lh
type="textarea"
@blur="normalizeExpectCompanies"
/>
</el-form-item>
</div>
<div mb42px>
<el-form-item mb0>
查看职位详情后是发起投递还是标记不合适的条件
<span font-size-12px>以下条件为空表示不筛选</span>
</el-form-item>
<div
flex
:style="{
alignItems: 'center',
justifyContent: 'space-between'
}"
>
<div>
<el-tooltip
effect="light"
placement="bottom"
@show="gtagRenderer('tooltip_show_about_expect_job_info_figure')"
>
<template #content>
<img block h-270px src="./resources/intro-of-job-info.png" />
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span>如下各信息位置图示</el-button
>
</el-tooltip>
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_mark_not_suit_intro')"
>
<template #content>
<ol m0 line-height-1.5em w-400px pl2em>
<li>
如果查找到的职位职位名称职位类型职位描述与如下正则不匹配则这个职位将被标记为不合适
</li>
<li>
如果查找到的职位活跃时间为本月活跃或更往前的时间则这个职位将被标记为不合适
</li>
<li>
如有错误标记请在左侧<a
href="javascript:void(0)"
style="color: var(--el-color-primary)"
@click.prevent="
() => {
gtagRenderer('click_view_mansr_from_boss_b_tooltip')
$router.push('/main-layout/MarkAsNotSuitRecord')
}
"
>标记不合适</a
>记录中找到相关记录手动对这些职位发起会话
</li>
</ol>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span>标记不合适机制</el-button
>
</el-tooltip>
</div>
<el-dropdown ml20px @command="handleExpectJobFilterTemplateClicked">
<el-button size="small"
>职位详情筛选模板按职类区分
<el-icon class="el-icon--right"><arrow-down /></el-icon
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in expectJobFilterTemplateList"
:key="item.name"
:command="item"
>{{ item.name }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div
:style="{
display: 'grid',
gridTemplateColumns: '1fr 1em 1fr 1em 1fr',
gap: '5px',
width: '100%',
alignItems: 'end'
}"
>
<el-form-item mb0 prop="expectJobNameRegExpStr">
<div font-size-12px>职位名称正则不区分大小写</div>
<el-input
v-model="formContent.expectJobNameRegExpStr"
@blur="
formContent.expectJobNameRegExpStr =
formContent.expectJobNameRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
<div mb10px font-size-12px flex flex-justify-center></div>
<el-form-item mb0 prop="expectJobTypeRegExpStr">
<div font-size-12px>职位类型正则不区分大小写</div>
<el-input
v-model="formContent.expectJobTypeRegExpStr"
@blur="
formContent.expectJobTypeRegExpStr =
formContent.expectJobTypeRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
<div mb10px font-size-12px flex flex-justify-center></div>
<el-form-item mb0 prop="expectJobDescRegExpStr">
<div font-size-12px>职位描述正则不区分大小写</div>
<el-input
v-model="formContent.expectJobDescRegExpStr"
@blur="
formContent.expectJobDescRegExpStr =
formContent.expectJobDescRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
</div>
</div>
<el-form-item
label="职位备选筛选条件(当前求职期望无合适职位时,自动更改Boss筛选条件,查找新工作)"
prop="filter"
mb0
>
<AnyCombineBossRecommendFilter v-model="formContent.anyCombineRecommendJobFilter" />
<div>
当前组合条件数{{ currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString() }}
<span
v-if="currentAnyCombineRecommendJobFilterCombinationCount >= 20"
class="color-orange"
>不建议选择太多组合条件</span
>
</div>
</el-form-item>
<el-form-item class="last-form-item mb0">
<el-button @click="handleSave">仅保存配置</el-button>
<el-button type="primary" @click="handleSubmit"> 保存配置并开始求职 </el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElForm, ElMessage } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import AnyCombineBossRecommendFilter from '@renderer/features/AnyCombineBossRecommendFilter/index.vue'
import { calculateTotalCombinations } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
import { gtagRenderer } from '@renderer/utils/gtag'
import defaultTargetCompanyListConf from '@geekgeekrun/geek-auto-start-chat-with-boss/default-config-file/target-company-list.json'
import { ArrowDown } from '@element-plus/icons-vue'
const router = useRouter()
const formContent = ref({
dingtalkRobotAccessToken: '',
expectCompanies: '',
anyCombineRecommendJobFilter: {},
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '',
expectJobDescRegExpStr: ''
})
const currentAnyCombineRecommendJobFilterCombinationCount = computed(() => {
return calculateTotalCombinations(formContent.value.anyCombineRecommendJobFilter)
})
electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
console.log(res)
formContent.value.dingtalkRobotAccessToken = res.config['dingtalk.json']['groupRobotAccessToken']
formContent.value.expectCompanies = res.config['target-company-list.json'].join(',')
formContent.value.anyCombineRecommendJobFilter = res.config['boss.json']
?.anyCombineRecommendJobFilter ?? {
salaryList: [],
experienceList: [],
degreeList: [],
scaleList: [],
industryList: []
}
//
if (
res.config['boss.json']?.expectJobRegExpStr &&
typeof res.config['boss.json']?.expectJobNameRegExpStr === 'undefined' &&
typeof res.config['boss.json']?.expectJobTypeRegExpStr === 'undefined' &&
typeof res.config['boss.json']?.expectJobDescRegExpStr === 'undefined'
) {
res.config['boss.json'].expectJobNameRegExpStr = res.config['boss.json'].expectJobRegExpStr
res.config['boss.json'].expectJobTypeRegExpStr = res.config['boss.json'].expectJobRegExpStr
res.config['boss.json'].expectJobDescRegExpStr = res.config['boss.json'].expectJobRegExpStr
}
formContent.value.expectJobNameRegExpStr = res.config['boss.json'].expectJobNameRegExpStr?.trim()
formContent.value.expectJobTypeRegExpStr = res.config['boss.json'].expectJobTypeRegExpStr?.trim()
formContent.value.expectJobDescRegExpStr = res.config['boss.json'].expectJobDescRegExpStr?.trim()
})
const formRules = {
expectJobNameRegExpStr: {
validator(_, value, cb) {
if (!value) {
cb()
gtagRenderer('empty_reg_exp_for_expect_job_name')
return
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_name')
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
gtagRenderer('invalid_reg_exp_for_expect_job_name')
}
}
},
expectJobTypeRegExpStr: {
validator(_, value, cb) {
if (!value) {
cb()
gtagRenderer('empty_reg_exp_for_expect_job_type')
return
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_type')
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
gtagRenderer('invalid_reg_exp_for_expect_job_type')
}
}
},
expectJobDescRegExpStr: {
validator(_, value, cb) {
if (!value) {
cb()
gtagRenderer('empty_reg_exp_for_expect_job_desc')
return
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_desc')
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
gtagRenderer('invalid_reg_exp_for_expect_job_desc')
}
}
}
}
const formRef = ref<InstanceType<typeof ElForm>>()
const handleSubmit = async () => {
gtagRenderer('save_config_and_launch_clicked', {
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
})
formContent.value.expectJobRegExpStr = undefined
try {
await formRef.value!.validate()
} catch (err) {
ElMessage.error({
message: '表单校验失败,请检查有误的内容',
grouping: true
})
console.log(err)
return
}
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
router.replace({
path: '/geekAutoStartChatWithBoss/prepareRun',
query: { flow: 'geek-auto-start-chat-with-boss' }
})
gtagRenderer('config_saved_and_launch_auto_start_chat', {
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
})
}
const handleSave = async () => {
gtagRenderer('save_config_clicked', {
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
})
normalizeExpectCompanies()
try {
await formRef.value!.validate()
} catch (err) {
ElMessage.error({
message: '表单校验失败,请检查有误的内容',
grouping: true
})
console.log(err)
return
}
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
ElMessage.success('配置保存成功')
gtagRenderer('config_saved')
}
const normalizeExpectCompanies = () => {
formContent.value.expectCompanies = formContent.value.expectCompanies
.split(/,|/)
.map((it) => it.trim())
.filter(Boolean)
.join(',')
}
const handleClickLaunchLogin = () => {
gtagRenderer('launch_login_clicked')
router.replace('/cookieAssistant')
}
const expectCompanyTemplateList = [
{
name: '默认值',
value: defaultTargetCompanyListConf.join(',')
},
{
name: '不限公司(随便投)',
value: ''
},
{
name: '大厂及关联企业',
value: `抖音,字节,字跳,有竹居,脸萌,头条,懂车帝,巨量引擎,滴滴,嘀嘀,小桔,网易,有道,腾讯,酷狗,酷我,阅文,搜狗,京东,沃东天骏,达达,达冠,百度,昆仑芯,小度,度小满,爱奇艺,携程,趣拿,去哪儿,集度,智图,长地万方,瑞图万方,道道通,小熊博望,理想,蔚来,顺丰,讯飞,同程,艺龙,马蜂窝,贝壳,自如,链家,我爱我家,相寓,多点,金山,小米,猎豹,新浪,微博,阿里,淘宝,淘麦郎,天猫,盒马,口碑,优视,夸克,UC,蚂蚁,高德,LAZADA,来赞达,飞猪,菜鸟,哈啰,钉钉,乌鸫,饿了么,美团,三快,猫眼,快手,映客,小红书,行吟,奇虎,360,三六零,鸿盈,奇富,奇元,亚信,启明星辰,奇安信,深信服,长亭,绿盟,天融信,商汤,SenseTime,大华,海康威视,hikvision,汽车之家,车好多,瓜子,易车,昆仑万维,昆仑天工,闲徕,趣加,FunPlus,完美,马上消费,轻松,水滴,白龙马,58,车欢欢,五八,红布林,致美,快狗,天鹅到家,转转,美餐,知乎,智者四海,易点云,搜狐,用友,畅捷通,猿辅导,小猿,猿力,好未来,学而思,希望学,新东方,东方甄选,东方优选,作业帮,高途,跟谁学,学科网,天学网,一起教育,一起作业,美术宝,火花思维,粉笔,老虎国际,一心向上,向上一意,联想,拉勾,乐视,欢聚,竞技世界,拼多多,寻梦,得物,Moka,希瑞亚斯,北森,OPPO,欧珀,vivo,维沃,小天才,步步高,读书郎,货拉拉,陌陌,探探,Shopee,首汽租车,GoFun,神州租车,天眼查,旷视,小冰,美图,智谱华章,MiniMax,石头科技,迅雷,TP,希音,SHEIN,稀宇,深言,百川智能,与爱为舞,牵手,Grab,爱回收,洋钱罐,瓴岳,得到,思维造物,地平线,咪咕,翼支付,电信,天翼,联通,蓝湖,墨刀`
},
{
name: '阿里系',
value: `阿里,淘宝,淘麦郎,天猫,盒马,口碑,优视,夸克,UC,蚂蚁,飞猪,乌鸫,饿了么,LAZADA,来赞达,菜鸟,哈啰,钉钉,高德,白龙马,新浪,微博`
},
{
name: '字节(头条/抖音)系',
value: `抖音,字节,字跳,有竹居,脸萌,头条,懂车帝,巨量引擎`
},
{
name: '百度系',
value: `百度,昆仑芯,小度,度小满,爱奇艺,携程,趣拿,去哪儿,集度,作业帮,智图,长地万方,瑞图万方,道道通,小熊博望`
},
{
name: '腾讯系',
value: `腾讯,酷狗,酷我,阅文,搜狗,京东,沃东天骏,达达,达冠,美团,三快,猫眼,快手,拼多多,寻梦,Shopee,滴滴,嘀嘀,小桔`
},
{
name: '外包、劳务派遣企业',
value: `青钱,软通动力,南天,睿服,中电金信,佰钧成,云链,博彦,汉克时代,柯莱特,拓保,亿达信息,纬创,微创,微澜,诚迈科技,法本,兆尹,诚迈,联合永道,新致软件,宇信科技,华为,德科,FESCO,科锐,科之锐`
}
]
function handleExpectCompanyTemplateClicked(item) {
gtagRenderer('expect_company_tpl_clicked', {
name: item.name
})
formContent.value.expectCompanies = item.value
}
const expectJobFilterTemplateList = [
{
name: '不限职位(随便投)',
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '',
expectJobDescRegExpStr: ''
}
},
{
name: '研发 - 前端开发工程师',
config: {
expectJobNameRegExpStr: '前端|H5|FE',
expectJobTypeRegExpStr: '前端开发|javascript',
expectJobDescRegExpStr: '前端|vue|react|node|js|javascript|H5'
}
},
{
name: '研发 - Java',
config: {
expectJobNameRegExpStr: '\\bJava\\b',
expectJobTypeRegExpStr: '\\bJava\\b',
expectJobDescRegExpStr: '\\bJava\\b|JVM|消息队列|MQ|MySQL|Nginx|Redis|Dubbo'
}
},
{
name: '人力 - 员工关系',
config: {
expectJobNameRegExpStr: '员工关系|劳动关系|SSC|人力资源|人资',
expectJobTypeRegExpStr: '员工关系|人力资源',
expectJobDescRegExpStr: '社保|考勤|入职|离职'
}
},
{
name: '人力 - 招聘',
config: {
expectJobNameRegExpStr: '招聘|招聘HR|招聘专员|招聘顾问|招聘专家|Recruiter|人力资源|人资',
expectJobTypeRegExpStr: '招聘|人力资源|猎头顾问',
expectJobDescRegExpStr: '简历筛选|面试安排|offer|猎头'
}
}
]
function handleExpectJobFilterTemplateClicked(item) {
gtagRenderer('expect_job_filter_tpl_clicked', {
name: item.name
})
Object.assign(formContent.value, {
...item.config
})
}
</script>
<style scoped lang="scss">
.form-wrap {
margin: 0 auto;
max-width: 1000px;
max-height: 100vh;
overflow: auto;
padding-left: 20px;
padding-right: 20px;
:deep(.el-form) {
padding-top: 8px;
}
.last-form-item {
:deep(.el-form-item__content) {
margin-top: 0px;
justify-content: flex-end;
}
}
}
</style>
<style lang="scss">
.form-wrap.geek-auto-start-run-with-boss {
.el-form-item__error.el-form-item__error {
font-size: 12px;
line-height: 1.2em;
}
}
</style>
@@ -0,0 +1,180 @@
<template>
<div>
<div v-if="modelValue?.length">
<div>当前已选择城市</div>
<div flex flex-wrap gap-10px>
<el-tag v-for="it in modelValue" :key="it">
{{ it }}
</el-tag>
</div>
</div>
<div v-else>
<div>当前未选择任何期望城市将不会按照城市进行筛选</div>
</div>
<div
line-height-1
:style="{
marginTop: modelValue?.length ? '10px' : ''
}"
>
<el-button
size="small"
type="primary"
@click="
() => {
isDialogVisible = true
gtagRenderer('choose_city_entry_button_clicked')
}
"
>选择城市</el-button
>
<el-button
v-if="modelValue?.length"
size="small"
type="danger"
@click="handleClearSelectedCitiesInModelValue"
>清空已选择的所有城市</el-button
>
</div>
<el-dialog
v-model="isDialogVisible"
width="1000px"
title="请选择城市"
:show-close="false"
@open="handleDialogOpen"
@closed="handleDialogClosed"
>
<el-tabs v-model="activeTabName">
<el-tab-pane
:style="{ height: '300px', overflow: 'auto' }"
label="热门城市"
name="热门城市"
>
<el-checkbox-group v-model="selectedCities">
<div
:style="{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr 1fr'
}"
>
<el-checkbox
v-for="op in hotCityList.filter((it) => it.code !== 100010000)"
:key="op.code"
:label="op.name"
>
{{ op.name }}
</el-checkbox>
</div>
</el-checkbox-group>
</el-tab-pane>
<el-tab-pane
v-for="it in cityGroupsByAlphabetMap.keys()"
:key="it"
:style="{ height: '300px', overflow: 'auto' }"
:label="it"
:value="it"
>
<div v-for="group in cityGroupsByAlphabetMap.get(it)" :key="group.firstChar">
{{ group.firstChar }}
<el-checkbox-group v-model="selectedCities">
<div
:style="{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr 1fr'
}"
>
<el-checkbox v-for="op in group.cityList" :key="op.code" :label="op.name">
{{ op.name }}
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<div
:style="{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}"
>
<div>
<el-button
v-if="selectedCities.length"
type="danger"
@click="handleClearSelectedCitiesInDialog"
>清空已选择的所有城市</el-button
>
</div>
<div>
<el-button @click="handleCancelClicked">取消</el-button>
<el-button type="primary" @click="handleConfirmClicked">确定</el-button>
</div>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { PropType, ref } from 'vue'
import cityGroupData from '../../../../../../common/constant/cityGroup.json'
import { gtagRenderer } from '@renderer/utils/gtag'
const props = defineProps({
modelValue: {
type: Array as PropType<string[]>
}
})
const emits = defineEmits(['update:modelValue'])
const { hotCityList, cityGroup } = cityGroupData.zpData
const activeTabName = ref('热门城市')
const isDialogVisible = ref(false)
const selectedCities = ref([])
const cityGroupsByAlphabetMap = ref(
new Map(['ABCDE', 'FGHJ', 'KLMN', 'PQRST', 'WXYZ'].map((it) => [it, []]))
)
for (const group of cityGroup) {
const { firstChar } = group
const targetKey =
[...cityGroupsByAlphabetMap.value.keys()].find((it) => it.includes(firstChar)) ?? null
if (!targetKey) {
if (!cityGroupsByAlphabetMap.value.get(targetKey)) {
cityGroupsByAlphabetMap.value.set(targetKey, [])
}
}
cityGroupsByAlphabetMap.value.get(targetKey)?.push(group)
}
function handleDialogOpen() {
activeTabName.value = '热门城市'
selectedCities.value = [...(props.modelValue ?? [])]
gtagRenderer('choose_city_dialog_open')
}
function handleCancelClicked() {
gtagRenderer('choose_city_cancel_button_clicked')
isDialogVisible.value = false
}
function handleConfirmClicked() {
gtagRenderer('choose_city_confirm_button_clicked', { value: selectedCities.value.join(',') })
isDialogVisible.value = false
emits('update:modelValue', [...(selectedCities.value ?? [])])
}
function handleDialogClosed() {
selectedCities.value = []
gtagRenderer('choose_city_dialog_closed')
}
function handleClearSelectedCitiesInModelValue() {
emits('update:modelValue', [])
gtagRenderer('clear_selected_cities_in_mv_clicked')
}
function handleClearSelectedCitiesInDialog() {
selectedCities.value = []
gtagRenderer('clear_selected_cities_in_dialog_clicked')
}
</script>
File diff suppressed because it is too large Load Diff
@@ -38,6 +38,18 @@
<template v-else-if="row.markReason === MarkAsNotSuitReason.JOB_NOT_SUIT"> <template v-else-if="row.markReason === MarkAsNotSuitReason.JOB_NOT_SUIT">
<strong>{{ markReasonTopicMap[row.markReason] }}</strong> <strong>{{ markReasonTopicMap[row.markReason] }}</strong>
</template> </template>
<template v-if="row.markReason === MarkAsNotSuitReason.JOB_CITY_NOT_SUIT">
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
</template>
<template v-if="row.markReason === MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT">
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
</template>
<template v-if="row.markReason === MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT">
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
</template>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn prop="experienceName" label="工作经验" /> <ElTableColumn prop="experienceName" label="工作经验" />
@@ -139,7 +151,7 @@ async function getMarkAsNotSuitRecord() {
try { try {
gtagRenderer('mansr_request_sent', { gtagRenderer('mansr_request_sent', {
page_no: pagination.value.pageNo, page_no: pagination.value.pageNo,
page_size: pagination.value.pageSize, page_size: pagination.value.pageSize
}) })
isTableLoading.value = true isTableLoading.value = true
const { data: res } = (await electron.ipcRenderer.invoke('get-mark-as-not-suit-record', { const { data: res } = (await electron.ipcRenderer.invoke('get-mark-as-not-suit-record', {
@@ -154,13 +166,13 @@ async function getMarkAsNotSuitRecord() {
} }
gtagRenderer('mansr_request_success', { gtagRenderer('mansr_request_success', {
page_no: pagination.value.pageNo, page_no: pagination.value.pageNo,
page_size: pagination.value.pageSize, page_size: pagination.value.pageSize
}) })
} catch (err) { } catch (err) {
gtagRenderer('mansr_request_error', { gtagRenderer('mansr_request_error', {
err, err,
page_no: pagination.value.pageNo, page_no: pagination.value.pageNo,
page_size: pagination.value.pageSize, page_size: pagination.value.pageSize
}) })
console.log(err) console.log(err)
tableData.value = [] tableData.value = []
@@ -204,7 +216,10 @@ function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) {
const markReasonTopicMap = { const markReasonTopicMap = {
[MarkAsNotSuitReason.BOSS_INACTIVE]: 'Boss不活跃', [MarkAsNotSuitReason.BOSS_INACTIVE]: 'Boss不活跃',
[MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON]: '手动标记不合适', [MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON]: '手动标记不合适',
[MarkAsNotSuitReason.JOB_NOT_SUIT]: '职位不合适' [MarkAsNotSuitReason.JOB_NOT_SUIT]: '职位不合适',
[MarkAsNotSuitReason.JOB_CITY_NOT_SUIT]: '工作地不合适',
[MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT]: '工作经验不合适',
[MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT]: '薪资不合适'
} }
function formatMarkReason(row: VMarkAsNotSuitLog) { function formatMarkReason(row: VMarkAsNotSuitLog) {
@@ -236,6 +251,34 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
.filter(Boolean) .filter(Boolean)
.join('\n') .join('\n')
} }
case MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT:
case MarkAsNotSuitReason.JOB_CITY_NOT_SUIT: {
const extInfo = (() => {
try {
return JSON.parse(row.extInfo)
} catch {
return null
}
})()
return [extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`]
.filter(Boolean)
.join('\n')
}
case MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT: {
const extInfo = (() => {
try {
return JSON.parse(row.extInfo)
} catch {
return null
}
})()
return [
extInfo?.salaryDesc && `薪资:${extInfo.salaryDesc}`,
extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`
]
.filter(Boolean)
.join('\n')
}
default: { default: {
return '' return ''
} }
@@ -11,6 +11,30 @@
>编辑Cookie</el-button >编辑Cookie</el-button
> >
</el-form-item> </el-form-item>
<el-form-item>
<div>
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
发送提醒消息前先按照Boss炸弹-职位类型正则校验正在与Boss沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<template v-else>
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithExpectJobType">
发送提醒消息前先按照Boss炸弹-职位类型正则校验正在与Boss沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前职位类型正则{{ expectJobTypeRegExpStr?.trim() }}</div>
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
<div>当前简历中填写的期望职位:{{ resumeContent?.expectJob ?? '-' }}</div>
<div color-orange>请确保上方二者信息匹配</div>
</template>
</div>
</template>
</div>
</el-form-item>
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的Boss时,将要向Boss发出:"> <el-form-item class="mb0" label="跟进话术 - 当发现已读不回的Boss时,将要向Boss发出:">
<el-radio-group v-model="formContent.autoReminder.rechatContentSource"> <el-radio-group v-model="formContent.autoReminder.rechatContentSource">
<div> <div>
@@ -150,7 +174,7 @@
@blur="handleThrottleIntervalMinutesBlur" @blur="handleThrottleIntervalMinutesBlur"
/>&nbsp;分钟内不多次跟进同一Boss />&nbsp;分钟内不多次跟进同一Boss
</el-form-item> </el-form-item>
<el-form-item label="跟进时限(天)" prop="rechatLimitDay"> <el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
<div> <div>
<div><el-checkbox v-model="enableRechatLimit" />&nbsp;启用</div> <div><el-checkbox v-model="enableRechatLimit" />&nbsp;启用</div>
<el-input-number <el-input-number
@@ -168,6 +192,30 @@
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span></div> <div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span></div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item>
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_stop_trace_one_boss')"
>
<template #content>
<ul m0 line-height-1.5em w-300px pl2em>
<li>
请向你不想继续提醒的Boss发送任意消息,发送后立即撤回的这条消息即可。
<br />
<br />
对于PC端Boss直聘,鼠标移动到要撤回的消息,点按鼠标右键调出菜单,再鼠标左键点击菜单中的“撤回”。如图所示:
<br />
<img block w-full src="./resources/withdraw-message-guide.png" />
</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>我不想持续提醒某个Boss了,如何处理?</el-button
>
</el-tooltip>
</el-form-item>
<el-form-item class="last-form-item"> <el-form-item class="last-form-item">
<el-button type="primary" @click="handleSubmit">开始提醒</el-button> <el-button type="primary" @click="handleSubmit">开始提醒</el-button>
</el-form-item> </el-form-item>
@@ -183,8 +231,15 @@ import {
RECHAT_CONTENT_SOURCE, RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK RECHAT_LLM_FALLBACK
} from '../../../../common/enums/auto-start-chat' } 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 router = useRouter()
const formContent = ref({ const formContent = ref({
autoReminder: { autoReminder: {
@@ -192,7 +247,8 @@ const formContent = ref({
rechatLimitDay: 21, rechatLimitDay: 21,
rechatContentSource: 1, rechatContentSource: 1,
recentMessageQuantityForLlm: 8, recentMessageQuantityForLlm: 8,
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION,
onlyRemindBossWithExpectJobType: true
} }
}) })
@@ -224,10 +280,32 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
? 8 ? 8
: parseInt(conf.recentMessageQuantityForLlm) : parseInt(conf.recentMessageQuantityForLlm)
: 8 : 8
conf.onlyRemindBossWithExpectJobType = conf.onlyRemindBossWithExpectJobType ?? true
conf.rechatLlmFallback = conf.rechatLlmFallback ?? RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION conf.rechatLlmFallback = conf.rechatLlmFallback ?? RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
formContent.value.autoReminder = conf formContent.value.autoReminder = conf
}) })
const expectJobTypeRegExpStr = ref('')
async function fetchExpectJobTypeRegExpStr() {
await electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
expectJobTypeRegExpStr.value = res.config['boss.json']?.expectJobTypeRegExpStr
})
}
fetchExpectJobTypeRegExpStr()
mittBus.on('auto-start-chat-with-boss-config-saved', fetchExpectJobTypeRegExpStr)
onUnmounted(() => {
mittBus.off('auto-start-chat-with-boss-config-saved', fetchExpectJobTypeRegExpStr)
})
const resumeContent = ref(null)
async function fetchResumeContent() {
await electron.ipcRenderer.invoke('fetch-resume-content').then((res) => {
resumeContent.value = res
})
}
fetchResumeContent()
const formRules = { const formRules = {
throttleIntervalMinutes: { throttleIntervalMinutes: {
validator(_, value, cb) { validator(_, value, cb) {
@@ -274,6 +352,7 @@ async function checkIsCanRun() {
gtagRenderer('invalid_rc_dialog_click_confirm') gtagRenderer('invalid_rc_dialog_click_confirm')
try { try {
await electron.ipcRenderer.invoke('resume-edit') await electron.ipcRenderer.invoke('resume-edit')
await fetchResumeContent()
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }
@@ -449,6 +528,7 @@ const handleClickEditResume = async () => {
gtagRenderer('edit_resume_clicked') gtagRenderer('edit_resume_clicked')
try { try {
await electron.ipcRenderer.invoke('resume-edit') await electron.ipcRenderer.invoke('resume-edit')
await fetchResumeContent()
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }
@@ -483,13 +563,13 @@ async function handleTestEffectClicked() {
<style scoped lang="scss"> <style scoped lang="scss">
.form-wrap { .form-wrap {
margin: 0 auto;
max-width: 1000px;
max-height: 100vh; max-height: 100vh;
overflow: auto; overflow: auto;
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
:deep(.el-form) { :deep(.el-form) {
margin: 0 auto;
max-width: 1000px;
padding-top: 8px; padding-top: 8px;
} }
.last-form-item { .last-form-item {
Binary file not shown.

After

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -322,11 +322,18 @@
import { ElForm, ElButton, ElAlert, ElMessageBox } from 'element-plus' import { ElForm, ElButton, ElAlert, ElMessageBox } from 'element-plus'
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-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' import { type ResumeContent, resumeContentEnoughDetect } from '../../../../common/utils/resume'
const formRef = ref<InstanceType<typeof ElForm>>() const formRef = ref<InstanceType<typeof ElForm>>()
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'resume-editor',
...params
})
}
const getEmptyFormContent = () => { const getEmptyFormContent = () => {
const o: any = { const o: any = {
expectJob: '', expectJob: '',
+1 -1
View File
@@ -45,7 +45,7 @@ const routes: Array<RouteRecordRaw> = [
children: [ children: [
{ {
path: 'GeekAutoStartChatWithBoss', path: 'GeekAutoStartChatWithBoss',
component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss.vue'), component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss/index.vue'),
meta: { meta: {
title: 'BOSS炸弹' title: 'BOSS炸弹'
} }
@@ -0,0 +1,3 @@
import mitt from 'mitt'
const mittBus = mitt()
export default mittBus
+32 -9
View File
@@ -129,9 +129,15 @@ importers:
electron-updater: electron-updater:
specifier: ^6.1.7 specifier: ^6.1.7
version: 6.1.7 version: 6.1.7
lodash-es:
specifier: ^4.17.21
version: 4.17.21
minimist: minimist:
specifier: ^1.2.8 specifier: ^1.2.8
version: 1.2.8 version: 1.2.8
mitt:
specifier: ^3.0.1
version: 3.0.1
node-machine-id: node-machine-id:
specifier: ^1.1.12 specifier: ^1.1.12
version: 1.1.12 version: 1.1.12
@@ -147,6 +153,9 @@ importers:
uuid: uuid:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0 version: 11.1.0
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.4.15)
devDependencies: devDependencies:
'@electron-toolkit/eslint-config': '@electron-toolkit/eslint-config':
specifier: ^1.0.2 specifier: ^1.0.2
@@ -206,8 +215,8 @@ importers:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0(vite@5.1.1) version: 2.0.0(vite@5.1.1)
element-plus: element-plus:
specifier: ^2.5.5 specifier: ^2.8.4
version: 2.5.5(vue@3.4.15) version: 2.8.4(vue@3.4.15)
eslint: eslint:
specifier: ^8.56.0 specifier: ^8.56.0
version: 8.56.0 version: 8.56.0
@@ -220,9 +229,6 @@ importers:
js-yaml: js-yaml:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
lodash-es:
specifier: ^4.17.21
version: 4.17.21
normalize.css: normalize.css:
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1 version: 8.0.1
@@ -3082,6 +3088,11 @@ packages:
/dayjs@1.11.10: /dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: false
/dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
dev: true
/de-indent@1.0.2: /de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
@@ -3357,8 +3368,8 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
/element-plus@2.5.5(vue@3.4.15): /element-plus@2.8.4(vue@3.4.15):
resolution: {integrity: sha512-yGU/MruLOWI0ImQPFCyFM4cXtHtILJNAi0hhLImcxjRukjgQLYNkvcvbwqNgBUt808KSeKF9MyxENFyBQLTg+Q==} resolution: {integrity: sha512-ZlVAdUOoJliv4kW3ntWnnSHMT+u/Os7mXJjk2xzOlqNeHaI2/ozlF+R58ZCEak8ZnDi6+5A2viWEYRsq64IuiA==}
peerDependencies: peerDependencies:
vue: ^3.2.0 vue: ^3.2.0
dependencies: dependencies:
@@ -3370,7 +3381,7 @@ packages:
'@types/lodash-es': 4.17.12 '@types/lodash-es': 4.17.12
'@vueuse/core': 9.13.0(vue@3.4.15) '@vueuse/core': 9.13.0(vue@3.4.15)
async-validator: 4.2.5 async-validator: 4.2.5
dayjs: 1.11.10 dayjs: 1.11.13
escape-html: 1.0.3 escape-html: 1.0.3
lodash: 4.17.21 lodash: 4.17.21
lodash-es: 4.17.21 lodash-es: 4.17.21
@@ -4559,7 +4570,6 @@ packages:
/lodash-es@4.17.21: /lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} 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): /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==} resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
@@ -5813,6 +5823,10 @@ packages:
ip: 2.0.0 ip: 2.0.0
smart-buffer: 4.2.0 smart-buffer: 4.2.0
/sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
dev: false
/source-map-js@1.0.2: /source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -6617,6 +6631,15 @@ packages:
'@vue/shared': 3.4.15 '@vue/shared': 3.4.15
typescript: 5.3.3 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: /web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'} engines: {node: '>= 8'}