mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-07-11 23:41:51 +08:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4cd4b884b | ||
|
|
61c28ae164 | ||
|
|
ece7fc61e3 | ||
|
|
97793adb0f | ||
|
|
b5c3116c01 | ||
|
|
10cd1cf11e | ||
|
|
7d2e60dfd0 | ||
|
|
6986fbdc3c | ||
|
|
5174da2c6c | ||
|
|
043d782a05 | ||
|
|
67e4b94f9e | ||
|
|
45d14e70fb | ||
|
|
44202b4685 | ||
|
|
00c699d1a6 | ||
|
|
a49124510b | ||
|
|
25e5253022 | ||
|
|
cd4a2e9694 | ||
|
|
cc6e7e9444 | ||
|
|
b650c7d86b | ||
|
|
e616b6668e | ||
|
|
1803b852db | ||
|
|
bde36317a1 | ||
|
|
28d2d34763 | ||
|
|
05702e773f | ||
|
|
6605c75ad7 | ||
|
|
1527a823c8 | ||
|
|
4dbc39008e | ||
|
|
1d604fc364 | ||
|
|
f6e1b799ce | ||
|
|
6f9533c2b7 | ||
|
|
7868f100ce | ||
|
|
a22ac20b3a | ||
|
|
e2c94ded13 | ||
|
|
84c30fc9a7 | ||
|
|
b88879a3bc | ||
|
|
33f6837147 | ||
|
|
9128865bbc | ||
|
|
8347fb0f77 | ||
|
|
7f21ea8c95 | ||
|
|
353fc60891 | ||
|
|
f770e7fe06 | ||
|
|
ccbc717a45 | ||
|
|
5dc7250634 | ||
|
|
5a1daf034f | ||
|
|
128ccfa93d | ||
|
|
582eae0900 | ||
|
|
d23b0475dc | ||
|
|
bbd670b674 | ||
|
|
3d148e8e01 | ||
|
|
954553d8d8 | ||
|
|
011c22e0fe | ||
|
|
4a82e9e345 | ||
|
|
f182ee2c5b | ||
|
|
3fc7662f7d | ||
|
|
e8d3313a7b | ||
|
|
e823590e6d | ||
|
|
04c02e148f | ||
|
|
dc293f4f91 | ||
|
|
f7fb551582 | ||
|
|
703b229bf4 | ||
|
|
64e9d3068c | ||
|
|
1d99cde602 | ||
|
|
7ffe89c41f | ||
|
|
bf0518036c | ||
|
|
f4634c5305 | ||
|
|
e838f48b89 | ||
|
|
94e237fee8 | ||
|
|
76339ebc5c | ||
|
|
6dc3948d12 | ||
|
|
764d278c23 | ||
|
|
73d03cb76a | ||
|
|
d42c651425 | ||
|
|
7b5ff9951b | ||
|
|
0e85942de5 | ||
|
|
462276cf6e | ||
|
|
1605103ee9 | ||
|
|
21c1f4284f | ||
|
|
4208996a83 | ||
|
|
3c951407ce | ||
|
|
438bc973a2 | ||
|
|
b7f149032a | ||
|
|
a8a1856527 | ||
|
|
1c616e5b90 | ||
|
|
e162bc4948 | ||
|
|
01ab679376 | ||
|
|
f91daf7cab | ||
|
|
f72a5c014b | ||
|
|
c5ac63b030 | ||
|
|
924febd5e0 | ||
|
|
17c5a40f8d | ||
|
|
09c5ce97a7 | ||
|
|
901c3a51a0 | ||
|
|
0714b187bd | ||
|
|
c40fbde5b2 | ||
|
|
27bc63f63f | ||
|
|
85e27dc6ef | ||
|
|
7458067708 | ||
|
|
5b78dc5d63 | ||
|
|
7ccf4303d3 | ||
|
|
7ee8baa311 | ||
|
|
653173e1a3 | ||
|
|
639ea6ac76 |
1
.github/workflows/release-ui.yml
vendored
1
.github/workflows/release-ui.yml
vendored
@@ -152,6 +152,7 @@ jobs:
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
prerelease: true
|
||||
allowUpdates: true
|
||||
artifacts: geekgeekrun-ui@${{ github.sha }}/*
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
147
README.md
147
README.md
@@ -4,12 +4,93 @@
|
||||
|
||||
与每一位牛人站在一起
|
||||
|
||||
- 使命:帮你找到让你满意的工作
|
||||
- 愿景:天下牛人都有满意的工作
|
||||
- 使命:用科技让复杂的求职过程变简单
|
||||
- 愿景:天下没有难找的工作,天下牛人都有满意的工作
|
||||
|
||||
# GUI 版本
|
||||
各行各业,无论你是小白还是大佬,都能通过几步简单的配置,快速开始求职!
|
||||
|
||||
## 程序有哪些功能?运行逻辑是什么?怎样使用?
|
||||
|
||||
### Boss炸弹
|
||||
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。
|
||||
|
||||
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ebf5c51c-f52c-4426-a60a-97916f663c6a" />
|
||||
|
||||
|
||||
#### 匹配步骤
|
||||
1. 按照公司名称查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
||||
2. 检查Boss活跃度
|
||||
- 如果Boss活跃度为本月活跃或更往前的时间,则会把职位标记为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
||||
3. 对职位名称、职位类型、职位描述进行匹配
|
||||
- 如果匹配则自动点击开聊按钮
|
||||
- 不匹配则标记这个职位为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
||||
|
||||
#### 异常情况
|
||||
- 当前页面筛选条件下,如果没有更多职位,则自动切换备选筛选条件,以获取更多新职位
|
||||
- 如当天开聊次数用完,本程序会暂停运行60分钟,之后尝试继续重新运行;如重新运行时间已在第二天,则将会继续开聊
|
||||
|
||||
#### 运行效果
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 已读不回提醒器
|
||||
Boss不明原因已读不回?简历就是投不出去?
|
||||
|
||||
已读不回提醒器,有事没事提醒一下已读不回的 Ta,助力把握每次机会
|
||||
|
||||
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/51a43b0f-118d-4b0a-957c-40a5367e29e4" />
|
||||
|
||||
|
||||
#### 匹配逻辑
|
||||
1. 在聊天列表中查找对你消息已读不回的Boss,再发一条消息,多次复聊;同时:
|
||||
- 如果设置了“跟进时限”,那么在这个时间之前活跃的聊天将不会被检查
|
||||
- 如果设置了“跟进间隔”,且再次检查时发现Boss已读不回,且距离上次提醒时间间隔小于这个时间,那么聊天将暂时不会跟进,直到下次检查时距离上次提醒时间间隔大于这个时间
|
||||
|
||||
#### 发送内容
|
||||
以下二者之一:
|
||||
1. “[盼回复]”表情
|
||||
2. 由大语言模型(根据简历及当前聊天上下文)生成的内容
|
||||
|
||||
## 辅助功能介绍
|
||||
### Boss 登录助手
|
||||
帮你用十分简单的方式,登录到 Boss 直聘,以进行自动开聊、已读不回提醒,避免和乱七八糟的技术名词(类似:Cookie、登录凭证、JSON……)打交道
|
||||
|
||||
<img width="1680" alt="image" src="https://github.com/user-attachments/assets/b5d02385-f1c0-4045-b01a-66c5f747fec5" />
|
||||
|
||||
|
||||
### 大语言模型设置
|
||||
设置本程序运行过程中可能会使用的大模型,有大语言模型加持,让求职更智能。支持配置多个备用模型,确保生成内容更随机,同时确保当某个模型不可用时也能继续让其他模型继续为你服务
|
||||
|
||||
<img width="576" alt="image" src="https://github.com/user-attachments/assets/ef91642f-b0b0-438f-8c66-e650f0de139b" />
|
||||
|
||||
|
||||
### 已读不回提醒器提示词模板编辑
|
||||
对生成的提醒消息不够满意?您不妨可以自己进行编辑提示词,来获得您期望的结果
|
||||
|
||||
<img width="1139" alt="image" src="https://github.com/user-attachments/assets/db956573-61c0-44d4-b588-5c04fe941f84" />
|
||||
|
||||
|
||||
### 已读不回提醒器提醒过程模拟
|
||||
想要测试配置的某一款模型可不可用?想测试使用当前配置生成的内容效果如何?那就来这里试试吧
|
||||
|
||||
<img width="600" alt="image" src="https://github.com/user-attachments/assets/3b2776bd-ae5b-48d1-a01c-0819509d96a1" />
|
||||
|
||||
|
||||
### 配置模板选项
|
||||
初次见面,不会配置本程序?当心配置出现问题?本程序为一些关键的功能配置(例如:Boss炸弹中的期望公司设置、职位详情筛选设置;大语言模型设置)加入了配置模板,让本程序更易用,助你轻松完成配置,开始求职。不妨来试试吧
|
||||
|
||||
<img width="576" alt="image" src="https://github.com/user-attachments/assets/2cccfece-17e7-4cc2-bf33-67f0ce500625" />
|
||||
|
||||
|
||||
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/f0c7453d-454e-494a-8c03-246107b1384f" />
|
||||
|
||||
|
||||
## 系统要求
|
||||
- 操作系统及处理器
|
||||
- Windows(x86_64):最低 Windows 10 1507(如果你的电脑是2016年后出厂、默认安装 Window 10 的电脑,一般都可以使用)
|
||||
@@ -36,16 +117,52 @@
|
||||
xattr -cr /Applications/GeekGeekRun.app
|
||||
```
|
||||
1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置
|
||||
<!--
|
||||
# CLI 版本
|
||||
## 使用方式
|
||||
1. 确保你已经安装 Node.js 18+、pnpm 8.15.9
|
||||
1. `pnpm i` 安装依赖
|
||||
1. 打开 Chrome / Edge,安装 EditThisCookie 扩展程序
|
||||
1. 打开 [Boss直聘](https://www.zhipin.com) 网站,在浏览器右上角找到 EditThisCookie 扩展程序图标,并点击
|
||||
1. 按下 EditThisCookie 扩展程序弹出气泡中的“导出Cookie”按钮(左数第三个按钮),此时将会把你在 Boss直聘 网站下所有Cookie复制到剪切板上
|
||||
1. 打开 `~/.geekgeekrun/storage/boss-cookies.json` 文件(如果没有相关路径请自行创建),把文件清空,并粘贴刚刚复制的 Cookie
|
||||
1. 执行`pnpm start`。开聊 Boss !
|
||||
-->
|
||||
|
||||
祝求职成功~
|
||||
## 为什么要写这个程序?
|
||||
### 个人而言
|
||||
以我为例,我真的求职求累了
|
||||
|
||||
2023年初~2025年初,这段时间
|
||||
- 求职经历:离职求职5个月,在职求职3个月;曾面试66个岗位,各种面试(一面、二面、三面、HR面)加起来134次;Offer 寥寥无几
|
||||
- 被动失业经历:1 次裁员,1 次卡试用期
|
||||
- 目前状态:有工作(但愿明天也有工作)
|
||||
|
||||
如此多的经历,已经让我成为了一位资深Boss用户
|
||||
|
||||
2023年,处于Gap期时,一天差不多可以有8小时浪费在Boss上挑选工作、开聊Boss上,这让我内心经常会很心累,时常陷入内耗,有种被全世界放弃的感觉
|
||||
|
||||
当时就有了想法,要让求职自动化。虽然后续也有新工作,但同工不同酬、卡试用期等情况,我还是有一些感受的。由此,便有了这个项目
|
||||
|
||||
我把之前的求职经验,通过这个程序表现了出来,假设你也在看机会,或许我的求职经验可以帮到你,让你少走弯路、减少内耗,愿你也能找到一份更好的工作
|
||||
|
||||
本程序完全公益、免费、开源。如需使用大模型功能,请自行到你喜欢的平台(例如:DeepSeek、阿里云、火山引擎、OpenAI)开通、充值,并在本程序中配置
|
||||
|
||||
### 大环境而言
|
||||
个人感受,大环境真的差,对于求职者相当不友好
|
||||
|
||||
一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场
|
||||
|
||||
当然,如果运气好:
|
||||
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个烂 Offer
|
||||
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了烂 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
|
||||
|
||||
为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧
|
||||
|
||||
因此,我编写了Boss炸弹,可以帮你尽可能多地将当日开聊机会用完;也编写了已读不回提醒器,帮你戳一戳列表里读了消息但无回应的 Boss
|
||||
|
||||
### 求职平台(Boss直聘)而言
|
||||
根据日常使用经验,Boss直聘在已经开聊很多职位的情况下,经常会推荐:
|
||||
1. 一些长时间不活跃的“僵尸”职位 - 活跃信息默认隐藏,需要点开职位详情才能看到
|
||||
2. 牛头不对马嘴,不符合求职期望的职位
|
||||
|
||||
人工筛选这些职位会有很大的心智负担,真的会吐……
|
||||
|
||||
因此,我在Boss炸弹代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位
|
||||
|
||||
--------
|
||||
|
||||
以上
|
||||
|
||||
更多功能还在开发中~
|
||||
|
||||
祝你求职成功,事业顺利,事事顺心
|
||||
|
||||
3
packages/geek-auto-start-chat-with-boss/constant.mjs
Normal file
3
packages/geek-auto-start-chat-with-boss/constant.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
export const activeDescList = [
|
||||
'', "半年前活跃","近半年活跃","5月内活跃","4月内活跃","3月内活跃","2月内活跃","本月活跃","2周内活跃","本周活跃","3日内活跃","昨日活跃","今日活跃","刚刚活跃"
|
||||
]
|
||||
@@ -7,8 +7,16 @@
|
||||
"industryList": []
|
||||
},
|
||||
"expectJobRegExpStr": "",
|
||||
"jobNotMatchStrategy": 1,
|
||||
"jobNotActiveStrategy": 1,
|
||||
"markAsNotActiveSelectedTimeRange": 7,
|
||||
"autoReminder": {
|
||||
"throttleIntervalMinutes": 10,
|
||||
"rechatLimitDay": 21
|
||||
"rechatLimitDay": 21,
|
||||
"geminiApiKey": "",
|
||||
"rechatContentSource": 1,
|
||||
"recentMessageQuantityForLlm": 8,
|
||||
"rechatLlmFallback": 1,
|
||||
"onlyRemindBossWithExpectJobType": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[{
|
||||
"providerCompleteApiUrl": "",
|
||||
"providerApiSecret": "",
|
||||
"model": "",
|
||||
"enabled": true,
|
||||
"serveWeight": 100,
|
||||
"_extra": {}
|
||||
}]
|
||||
@@ -1,4 +1,4 @@
|
||||
[
|
||||
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技",
|
||||
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","京东","沃东天骏","达达","百度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","理想","蔚来","顺丰","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","蚂蚁","飞猪","高德","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","得物","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","首汽租车","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技"
|
||||
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
|
||||
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","巨量引擎","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","小鹅通","富途","京东","沃东天骏","达达","达冠","百度","昆仑芯","小度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","智图","长地万方","瑞图万方","道道通","小熊博望","理想","蔚来","顺丰","丰巢","中通","圆通","申通","跨越","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","淘宝","淘麦郎","天猫","盒马","口碑","优视","夸克","UC","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","51talk","爱学习","高思","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","从鲸","TEMU","得物","有赞","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","虾皮","首汽租车","GoFun","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","锐捷","Tenda","腾达","斐讯","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手","Grab","爱回收","洋钱罐","瓴岳","得到","思维造物","地平线","咪咕","翼支付","电信","天翼","联通","蓝湖","墨刀","海尔","美的","米哈游","传音","同花顺","国美","TCL"
|
||||
]
|
||||
@@ -16,7 +16,8 @@ import { calculateTotalCombinations, combineFiltersWithConstraintsGenerator } fr
|
||||
import { default as jobFilterConditions } from './internal-config/job-filter-conditions-20241002.json'
|
||||
import { default as rawIndustryFilterExemption } from './internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||||
import { ChatStartupFrom } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatStartupLog'
|
||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import { MarkAsNotSuitReason, MarkAsNotSuitOp } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import { activeDescList } from './constant.mjs'
|
||||
|
||||
const jobFilterConditionsMapByCode = {}
|
||||
Object.values(jobFilterConditions).forEach(arr => {
|
||||
@@ -79,9 +80,42 @@ const targetCompanyList = readConfigFile('target-company-list.json').filter(it =
|
||||
|
||||
const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter
|
||||
const expectJobRegExpStr = readConfigFile('boss.json').expectJobRegExpStr
|
||||
const jobNotMatchStrategy = readConfigFile('boss.json').jobNotMatchStrategy ?? MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
const markAsNotActiveSelectedTimeRange = (() => {
|
||||
let n = readConfigFile('boss.json').markAsNotActiveSelectedTimeRange
|
||||
if (
|
||||
typeof n !== 'number' || isNaN(parseInt(n)) || n >= activeDescList.length || n < 0
|
||||
) {
|
||||
n = 7
|
||||
}
|
||||
return n
|
||||
})()
|
||||
const jobNotActiveStrategy = (() => {
|
||||
let value = readConfigFile('boss.json').jobNotActiveStrategy ?? MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
if (markAsNotActiveSelectedTimeRange === 0) {
|
||||
value = MarkAsNotSuitOp.NO_OP
|
||||
}
|
||||
return value
|
||||
})()
|
||||
|
||||
let {
|
||||
expectJobNameRegExpStr,
|
||||
expectJobTypeRegExpStr,
|
||||
expectJobDescRegExpStr,
|
||||
} = readConfigFile('boss.json')
|
||||
if (
|
||||
expectJobRegExpStr &&
|
||||
!expectJobNameRegExpStr &&
|
||||
!expectJobTypeRegExpStr &&
|
||||
!expectJobDescRegExpStr
|
||||
) {
|
||||
expectJobNameRegExpStr = expectJobRegExpStr
|
||||
expectJobTypeRegExpStr = expectJobRegExpStr
|
||||
expectJobDescRegExpStr = expectJobRegExpStr
|
||||
}
|
||||
|
||||
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
|
||||
const recommendJobPageUrl = `https://www.zhipin.com/web/geek/job-recommend`
|
||||
const recommendJobPageUrl = `https://www.zhipin.com/web/geek/jobs`
|
||||
|
||||
const expectCompanySet = new Set(targetCompanyList)
|
||||
const enableCompanyAllowList = Boolean(expectCompanySet.size)
|
||||
@@ -106,6 +140,12 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
||||
const result = {}
|
||||
const notSuitableFeedbackButtonProxy = await page.$('.job-detail-box .job-detail-operate .not-suitable')
|
||||
if (notSuitableFeedbackButtonProxy) {
|
||||
await notSuitableFeedbackButtonProxy.evaluate(el => {
|
||||
el.scrollIntoView({
|
||||
block: 'center'
|
||||
})
|
||||
})
|
||||
await sleep(200)
|
||||
await notSuitableFeedbackButtonProxy.click()
|
||||
const rawReasonResData = (await (await page.waitForResponse(
|
||||
response => {
|
||||
@@ -191,22 +231,32 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
||||
return result
|
||||
}
|
||||
|
||||
export function testIfJobTitleOrDescriptionSuit (jobInfo, regExpStr) {
|
||||
if (!regExpStr) {
|
||||
return true
|
||||
}
|
||||
export function testIfJobTitleOrDescriptionSuit (jobInfo) {
|
||||
let isJobNameSuit = true
|
||||
try {
|
||||
const regExp = new RegExp(regExpStr, 'i')
|
||||
if (
|
||||
!regExp.test(jobInfo.jobName)
|
||||
&& !regExp.test(jobInfo.positionName)
|
||||
&& !regExp.test(jobInfo.postDescription)
|
||||
) {
|
||||
return false
|
||||
if (expectJobNameRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobNameRegExpStr, 'i')
|
||||
isJobNameSuit = regExp.test(jobInfo.jobName)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return true
|
||||
let isJobTypeSuit = true
|
||||
try {
|
||||
if (expectJobTypeRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobTypeRegExpStr, 'i')
|
||||
isJobTypeSuit = regExp.test(jobInfo.positionName)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
let isJobDescSuit = true
|
||||
try {
|
||||
if (expectJobDescRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobDescRegExpStr, 'i')
|
||||
isJobDescSuit = regExp.test(jobInfo.postDescription)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return isJobNameSuit && isJobTypeSuit && isJobDescSuit
|
||||
}
|
||||
|
||||
async function setFilterCondition (selectedFilters) {
|
||||
@@ -239,7 +289,7 @@ async function setFilterCondition (selectedFilters) {
|
||||
const placeholderText = placeholderTexts[i]
|
||||
const filterDropdownProxy = await (async () => {
|
||||
const jsHandle = (await page.evaluateHandle((placeholderText) => {
|
||||
const filterBar = document.querySelector('.job-recommend-main .job-recommend-search')
|
||||
const filterBar = document.querySelector('.page-jobs-main .filter-condition-inner')
|
||||
const dropdownEntry = filterBar.__vue__.$children.find(it => it.placeholder === placeholderText)
|
||||
return dropdownEntry.$el
|
||||
}, placeholderText)).asElement();
|
||||
@@ -264,18 +314,18 @@ async function setFilterCondition (selectedFilters) {
|
||||
const optionKaPrefix = optionKaPrefixes[i]
|
||||
if (!currentFilterConditions.length) {
|
||||
if (placeholderText === '公司行业') {
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.job-recommend-main .recommend-search-more .active[ka^="${optionKaPrefix}"]`)
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||||
for (const it of activeOptionElAtCurrentFilterProxyList) {
|
||||
await it.click()
|
||||
}
|
||||
} else {
|
||||
// select 不限 immediately
|
||||
const buxianOptionElProxy = await page.$(`.job-recommend-main .recommend-search-more [ka="${optionKaPrefix}${0}"]`)
|
||||
const buxianOptionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${0}"]`)
|
||||
await buxianOptionElProxy.click()
|
||||
}
|
||||
} else {
|
||||
//#region uncheck options perviously checked but not existed in current filter.
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.job-recommend-main .recommend-search-more .active[ka^="${optionKaPrefix}"]`)
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||||
const activeOptionValues = (await Promise.all(
|
||||
activeOptionElAtCurrentFilterProxyList.map(elProxy => {
|
||||
return elProxy.evaluate((el) => {
|
||||
@@ -314,7 +364,7 @@ async function setFilterCondition (selectedFilters) {
|
||||
optionValue = conditionToCheck[j]
|
||||
}
|
||||
await sleepWithRandomDelay(500)
|
||||
const optionElProxy = await page.$(`.job-recommend-main .recommend-search-more [ka="${optionKaPrefix}${optionValue}"]`)
|
||||
const optionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${optionValue}"]`)
|
||||
if (!optionElProxy) {
|
||||
continue;
|
||||
}
|
||||
@@ -347,7 +397,7 @@ async function toRecommendPage (hooks) {
|
||||
return res.json()
|
||||
})
|
||||
await Promise.all([
|
||||
page.goto(recommendJobPageUrl, { timeout: 60 * 1000 }),
|
||||
page.goto(recommendJobPageUrl, { timeout: 120 * 1000 }),
|
||||
page.waitForNavigation(),
|
||||
])
|
||||
if (
|
||||
@@ -384,7 +434,7 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
}
|
||||
|
||||
const INIT_START_EXCEPT_JOB_INDEX = 1
|
||||
const INIT_START_EXCEPT_JOB_INDEX = 0
|
||||
let currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||||
afterPageLoad: while (true) {
|
||||
let expectJobList
|
||||
@@ -397,20 +447,21 @@ async function toRecommendPage (hooks) {
|
||||
await sleepWithRandomDelay(2500)
|
||||
|
||||
await Promise.all([
|
||||
page.waitForSelector('.job-recommend-main .recommend-search-expect .recommend-job-btn'),
|
||||
page.waitForSelector('.c-expect-select .expect-list .expect-item'),
|
||||
page.waitForSelector('.job-list-container .rec-job-list')
|
||||
])
|
||||
await page.click(`.c-expect-select .expect-list .expect-item`)
|
||||
const currentActiveJobIndex = await page.evaluate(`
|
||||
[...document.querySelectorAll('.job-recommend-main .recommend-search-expect .recommend-job-btn')].findIndex(it => it.classList.contains('active'))
|
||||
[...document.querySelectorAll('.c-expect-select .expect-list .expect-item')].findIndex(it => it.classList.contains('active'))
|
||||
`)
|
||||
|
||||
expectJobList = await page.evaluate(`document.querySelector('.job-recommend-search')?.__vue__?.expectList`)
|
||||
expectJobList = await page.evaluate(`document.querySelector('.c-expect-select')?.__vue__?.expectList`)
|
||||
if (currentActiveJobIndex === currentExceptJobIndex) {
|
||||
// first navigation and can immediately start chat (recommend job)
|
||||
} else {
|
||||
// not first navigation and should choose a job (except job)
|
||||
// click first expect job
|
||||
const expectJobTabHandlers = await page.$$('.job-recommend-main .recommend-search-expect .recommend-job-btn')
|
||||
const expectJobTabHandlers = await page.$$('.c-expect-select .expect-list .expect-item')
|
||||
await expectJobTabHandlers[currentExceptJobIndex].click()
|
||||
await page.waitForResponse(
|
||||
response => {
|
||||
@@ -461,22 +512,28 @@ async function toRecommendPage (hooks) {
|
||||
|
||||
// job list
|
||||
const recommendJobListElProxy = await page.$('.job-list-container .rec-job-list')
|
||||
let jobListData = await page.evaluate(`document.querySelector('.job-recommend-main')?.__vue__?.jobList`)
|
||||
let jobListData = await page.evaluate(`document.querySelector('.page-jobs-main')?.__vue__?.jobList`)
|
||||
let hasReachLastPage = false
|
||||
let targetJobIndex = -1
|
||||
let targetJobData
|
||||
function getTempTargetJobIndexToCheckDetail () {
|
||||
return jobListData.findIndex(it =>
|
||||
!blockBossNotNewChat.has(it.encryptBossId) &&
|
||||
!blockBossNotActive.has(it.encryptBossId) &&
|
||||
!blockJobNotSuit.has(it.encryptJobId) &&
|
||||
(
|
||||
enableCompanyAllowList ?
|
||||
[...expectCompanySet].find(
|
||||
name => it.brandName?.toLowerCase?.()?.includes(name.toLowerCase())
|
||||
)
|
||||
:
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
continueFind: while (targetJobIndex < 0 && !hasReachLastPage) {
|
||||
// when disable company allow list, we will believe that the first one in the list is your expect job.
|
||||
let tempTargetJobIndexToCheckDetail = enableCompanyAllowList ? jobListData.findIndex(
|
||||
it => !blockBossNotNewChat.has(it.encryptBossId)
|
||||
&& !blockBossNotActive.has(it.encryptBossId)
|
||||
&& [...expectCompanySet].find(
|
||||
name => it.brandName.includes(name)
|
||||
)
|
||||
&& !blockJobNotSuit.has(it.encryptJobId)
|
||||
) : jobListData.findIndex(
|
||||
it => !blockBossNotNewChat.has(it.encryptBossId) && !blockBossNotActive.has(it.encryptBossId) && !blockJobNotSuit.has(it.encryptJobId)
|
||||
)
|
||||
let tempTargetJobIndexToCheckDetail = getTempTargetJobIndexToCheckDetail()
|
||||
while (tempTargetJobIndexToCheckDetail < 0 && !hasReachLastPage) {
|
||||
// fetch new
|
||||
const recommendJobListElBBox = await recommendJobListElProxy.boundingBox()
|
||||
@@ -494,10 +551,10 @@ async function toRecommendPage (hooks) {
|
||||
) {
|
||||
scrolledHeight += increase
|
||||
await page.mouse.wheel({deltaY: increase});
|
||||
await sleep(1)
|
||||
await sleep(100)
|
||||
await requestNextPagePromiseWithResolver?.promise
|
||||
hasReachLastPage = await page.evaluate(`
|
||||
!(document.querySelector('.job-recommend-main')?.__vue__?.hasMore)
|
||||
!(document.querySelector('.page-jobs-main')?.__vue__?.hasMore)
|
||||
`)
|
||||
if (hasReachLastPage) {
|
||||
console.log(`Arrive the terminal of the job list.`)
|
||||
@@ -505,18 +562,13 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
requestNextPagePromiseWithResolver = null
|
||||
|
||||
await sleep(3000)
|
||||
await sleep(5000)
|
||||
jobListData = await page.evaluate(
|
||||
`
|
||||
document.querySelector('.job-recommend-main')?.__vue__?.jobList
|
||||
document.querySelector('.page-jobs-main')?.__vue__?.jobList
|
||||
`
|
||||
)
|
||||
tempTargetJobIndexToCheckDetail = jobListData.findIndex(it =>
|
||||
!blockBossNotNewChat.has(it.encryptBossId) &&
|
||||
!blockBossNotActive.has(it.encryptBossId) &&
|
||||
[...expectCompanySet].find(name => it.brandName.includes(name)) &&
|
||||
!blockJobNotSuit.has(it.encryptJobId)
|
||||
)
|
||||
tempTargetJobIndexToCheckDetail = getTempTargetJobIndexToCheckDetail()
|
||||
}
|
||||
|
||||
if (tempTargetJobIndexToCheckDetail < 0 && hasReachLastPage) {
|
||||
@@ -543,6 +595,7 @@ async function toRecommendPage (hooks) {
|
||||
const recommendJobItemList = await recommendJobListElProxy.$$('ul.rec-job-list li.job-card-box')
|
||||
const targetJobElProxy = recommendJobItemList[tempTargetJobIndexToCheckDetail]
|
||||
// click that element
|
||||
await sleep(500)
|
||||
await targetJobElProxy.click()
|
||||
await page.waitForResponse(
|
||||
response => {
|
||||
@@ -565,54 +618,83 @@ async function toRecommendPage (hooks) {
|
||||
// 刚刚活跃 // 今日活跃 // 昨日活跃 // 3日内活跃 // 本周活跃 // 2周内活跃
|
||||
// 本月活跃 // 2月内活跃 // 3月内活跃 // 4月内活跃 // 5月内活跃 // 近半年活跃 // 半年前活跃
|
||||
//#endregion
|
||||
if ([
|
||||
'本月活跃',
|
||||
'2月内活跃',
|
||||
'3月内活跃',
|
||||
'4月内活跃',
|
||||
'5月内活跃',
|
||||
'近半年活跃',
|
||||
'半年前活跃',
|
||||
].includes(targetJobData.bossInfo.activeTimeDesc)) {
|
||||
const indexOfActiveText = activeDescList.indexOf(targetJobData.bossInfo.activeTimeDesc)
|
||||
if (
|
||||
markAsNotActiveSelectedTimeRange > 0 &&
|
||||
indexOfActiveText > 0 && indexOfActiveText <= markAsNotActiveSelectedTimeRange
|
||||
) {
|
||||
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
|
||||
// click prevent recommend button
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.BOSS_INACTIVE)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||||
extInfo: {
|
||||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||||
chosenReasonInUi
|
||||
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.BOSS_INACTIVE)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||||
extInfo: {
|
||||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
continue continueFind
|
||||
}
|
||||
if (
|
||||
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo, expectJobRegExpStr)
|
||||
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo)
|
||||
) {
|
||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||||
extInfo: {
|
||||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||||
chosenReasonInUi
|
||||
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||||
extInfo: {
|
||||
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
|
||||
chosenReasonInUi
|
||||
},
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
else if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
|
||||
try {
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
{
|
||||
markFrom: ChatStartupFrom.AutoFromRecommendList,
|
||||
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
|
||||
extInfo: null,
|
||||
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
debugger
|
||||
continue continueFind
|
||||
}
|
||||
const startChatButtonInnerHTML = await page.evaluate('document.querySelector(".job-detail-box .op-btn.op-btn-chat")?.innerHTML.trim()')
|
||||
@@ -646,6 +728,7 @@ async function toRecommendPage (hooks) {
|
||||
|
||||
await hooks.newChatWillStartup?.promise(targetJobData)
|
||||
const startChatButtonProxy = await page.$('.job-detail-box .op-btn.op-btn-chat')
|
||||
await sleep(500)
|
||||
//#region click the chat button
|
||||
await startChatButtonProxy.click()
|
||||
|
||||
@@ -714,9 +797,11 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
// for of reach terminal
|
||||
if (
|
||||
currentExceptJobIndex + 1 > expectJobList.length
|
||||
currentExceptJobIndex + 1 >= expectJobList.length
|
||||
) {
|
||||
hooks.noPositionFoundForCurrentJob?.call()
|
||||
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
||||
await sleep((20 + 30 * Math.random()) * 1000)
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForNavigation()
|
||||
@@ -724,8 +809,7 @@ async function toRecommendPage (hooks) {
|
||||
currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||||
} else {
|
||||
hooks.noPositionFoundForCurrentJob?.call()
|
||||
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
||||
|
||||
await sleep((10 + 15 * Math.random()) * 1000)
|
||||
currentExceptJobIndex += 1
|
||||
}
|
||||
}
|
||||
@@ -745,7 +829,6 @@ export async function mainLoop (hooks) {
|
||||
}
|
||||
})
|
||||
hooks.puppeteerLaunched?.call()
|
||||
|
||||
page = (await browser.pages())[0]
|
||||
//set cookies
|
||||
hooks.cookieWillSet?.call(bossCookies)
|
||||
@@ -754,6 +837,12 @@ export async function mainLoop (hooks) {
|
||||
}
|
||||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
|
||||
await page.bringToFront()
|
||||
await hooks.mainFlowWillLaunch?.callAsync({
|
||||
jobNotMatchStrategy,
|
||||
jobNotActiveStrategy,
|
||||
blockJobNotSuit,
|
||||
blockBossNotActive,
|
||||
})
|
||||
await toRecommendPage(hooks)
|
||||
// goto search
|
||||
|
||||
|
||||
@@ -6,16 +6,18 @@ import os from 'node:os'
|
||||
import defaultDingtalkConf from './default-config-file/dingtalk.json' assert {type: 'json'}
|
||||
import defaultBossConf from './default-config-file/boss.json' assert {type: 'json'}
|
||||
import defaultTargetCompanyListConf from './default-config-file/target-company-list.json' assert {type: 'json'}
|
||||
import defaultLlmConf from './default-config-file/llm.json' assert { type: 'json' }
|
||||
|
||||
import defaultBossCookieStorage from './default-storage-file/boss-cookies.json' assert { type: 'json' }
|
||||
import defaultBossLocalStorageStorage from './default-storage-file/boss-local-storage.json' assert { type: 'json' }
|
||||
import defaultJobNotSuitReasonCodeToTextCacheStorage from './default-storage-file/job-not-suit-reason-code-to-text-cache.json' assert { type: 'json' }
|
||||
export const configFileNameList = ['boss.json', 'dingtalk.json', 'target-company-list.json']
|
||||
export const configFileNameList = ['boss.json', 'dingtalk.json', 'target-company-list.json', 'llm.json']
|
||||
|
||||
const defaultConfigFileContentMap = {
|
||||
'boss.json': JSON.stringify(defaultBossConf),
|
||||
'dingtalk.json': JSON.stringify(defaultDingtalkConf),
|
||||
'target-company-list.json': JSON.stringify(defaultTargetCompanyListConf)
|
||||
'target-company-list.json': JSON.stringify(defaultTargetCompanyListConf),
|
||||
'llm.json': JSON.stringify(defaultLlmConf)
|
||||
}
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
@@ -69,8 +71,12 @@ export const readConfigFile = (fileName) => {
|
||||
)
|
||||
} catch {
|
||||
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
||||
ensureConfigFileExist()
|
||||
o = JSON.parse(defaultConfigFileContentMap[fileName])
|
||||
if (defaultConfigFileContentMap[fileName]) {
|
||||
ensureConfigFileExist()
|
||||
o = JSON.parse(defaultConfigFileContentMap[fileName])
|
||||
} else {
|
||||
o = null
|
||||
}
|
||||
}
|
||||
|
||||
return o
|
||||
@@ -112,7 +118,8 @@ export const ensureStorageFileExist = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const readStorageFile = (fileName) => {
|
||||
export const readStorageFile = (fileName, { isJson } = {}) => {
|
||||
isJson = isJson ?? true
|
||||
const joinedPath = path.join(storageFilePath, fileName)
|
||||
|
||||
if (!fs.existsSync(
|
||||
@@ -123,21 +130,36 @@ export const readStorageFile = (fileName) => {
|
||||
|
||||
let o
|
||||
try {
|
||||
o = JSON.parse(
|
||||
fs.readFileSync(joinedPath)
|
||||
)
|
||||
const content = fs.readFileSync(joinedPath)
|
||||
if (isJson) {
|
||||
o = JSON.parse(content)
|
||||
}
|
||||
else {
|
||||
o = content.toString()
|
||||
}
|
||||
} catch {
|
||||
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
||||
ensureStorageFileExist()
|
||||
o = JSON.parse(defaultStorageFileContentMap[fileName])
|
||||
if (isJson) {
|
||||
o = JSON.parse(defaultStorageFileContentMap[fileName] ?? 'null')
|
||||
}
|
||||
else {
|
||||
o = defaultStorageFileContentMap[fileName] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
export const writeStorageFile = async (fileName, content) => {
|
||||
export const writeStorageFile = async (fileName, content, { isJson } = {}) => {
|
||||
isJson = isJson ?? true
|
||||
const filePath = path.join(storageFilePath, fileName)
|
||||
const fileContent = JSON.stringify(content)
|
||||
let fileContent
|
||||
if (isJson) {
|
||||
fileContent = JSON.stringify(content)
|
||||
} else {
|
||||
fileContent = content
|
||||
}
|
||||
return fsPromise.writeFile(
|
||||
filePath,
|
||||
fileContent
|
||||
|
||||
Binary file not shown.
@@ -6,68 +6,28 @@ import {
|
||||
sleep,
|
||||
sleepWithRandomDelay
|
||||
} from '@geekgeekrun/utils/sleep.mjs'
|
||||
import extractZip from 'extract-zip'
|
||||
import { blockNavigation } from '@geekgeekrun/utils/puppeteer/block-navigation.mjs'
|
||||
import {
|
||||
writeStorageFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path';
|
||||
import JSON5 from 'json5'
|
||||
import url from 'url';
|
||||
import packageJson from './package.json' assert {type: 'json'}
|
||||
import {
|
||||
runtimeFolderPath,
|
||||
ensureEditThisCookie,
|
||||
editThisCookieExtensionPath,
|
||||
isRunFromUi,
|
||||
} from './utils.mjs'
|
||||
|
||||
import { EventEmitter } from 'node:events'
|
||||
|
||||
export const loginEventBus = new EventEmitter()
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
const isUiDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
const extensionDir = path.join(
|
||||
runtimeFolderPath,
|
||||
'chrome-extensions'
|
||||
)
|
||||
if (!fs.existsSync(
|
||||
runtimeFolderPath
|
||||
)) {
|
||||
fs.mkdirSync(runtimeFolderPath)
|
||||
}
|
||||
if (!fs.existsSync(extensionDir)) {
|
||||
fs.mkdirSync(extensionDir)
|
||||
}
|
||||
const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
|
||||
|
||||
let editThisCookieZipPath
|
||||
async function getEditThisCookieZipPath () {
|
||||
if (editThisCookieZipPath) {
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
if (isRunFromUi) {
|
||||
const { app } = await import('electron')
|
||||
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
|
||||
} else {
|
||||
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
|
||||
}
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
if (!fs.existsSync(
|
||||
path.join(editThisCookieExtensionPath, 'manifest.json')
|
||||
)) {
|
||||
await extractZip(
|
||||
await getEditThisCookieZipPath(),
|
||||
{
|
||||
dir: extensionDir
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
await ensureEditThisCookie()
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import extractZip from 'extract-zip'
|
||||
import packageJson from './package.json' assert {type: 'json'}
|
||||
|
||||
export const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
const isUiDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
export const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
|
||||
const extensionDir = path.join(
|
||||
runtimeFolderPath,
|
||||
'chrome-extensions'
|
||||
)
|
||||
if (!fs.existsSync(
|
||||
runtimeFolderPath
|
||||
)) {
|
||||
fs.mkdirSync(runtimeFolderPath)
|
||||
}
|
||||
if (!fs.existsSync(extensionDir)) {
|
||||
fs.mkdirSync(extensionDir)
|
||||
}
|
||||
export const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
|
||||
|
||||
let editThisCookieZipPath
|
||||
async function getEditThisCookieZipPath () {
|
||||
if (editThisCookieZipPath) {
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
if (isRunFromUi) {
|
||||
const { app } = await import('electron')
|
||||
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
|
||||
} else {
|
||||
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
|
||||
}
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
|
||||
export async function ensureEditThisCookie () {
|
||||
let isNeedExtractEditThisCookie = false
|
||||
const manifestFilePath = path.join(editThisCookieExtensionPath, 'manifest.json')
|
||||
if (!fs.existsSync(
|
||||
manifestFilePath
|
||||
)) {
|
||||
isNeedExtractEditThisCookie = true
|
||||
} else {
|
||||
let manifest
|
||||
try {
|
||||
manifest = JSON.parse(fs.readFileSync(manifestFilePath, { encoding: 'utf-8' }))
|
||||
if (!manifest.manifest_version || manifest.manifest_version <= 2) {
|
||||
isNeedExtractEditThisCookie = true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.log(`未能获取到文件内容`)
|
||||
isNeedExtractEditThisCookie = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isNeedExtractEditThisCookie) {
|
||||
if (
|
||||
fs.existsSync(
|
||||
editThisCookieExtensionPath
|
||||
)
|
||||
) {
|
||||
fs.rmSync(
|
||||
editThisCookieExtensionPath,
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
)
|
||||
}
|
||||
await extractZip(
|
||||
await getEditThisCookieZipPath(),
|
||||
{
|
||||
dir: extensionDir
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ const main = async () => {
|
||||
pageLoaded: new SyncHook(),
|
||||
cookieWillSet: new SyncHook(['cookies']),
|
||||
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
||||
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
|
||||
newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']),
|
||||
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
|
||||
noPositionFoundForCurrentJob: new SyncHook(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@geekgeekrun/sqlite-plugin",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"dependencies": {
|
||||
|
||||
@@ -25,7 +25,7 @@ export class ChatMessageRecord {
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
style?: 'sent' | 'receive';
|
||||
style?: 'sent' | 'received';
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
|
||||
64
packages/sqlite-plugin/src/entity/LlmModelUsageRecord.ts
Normal file
64
packages/sqlite-plugin/src/entity/LlmModelUsageRecord.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, PrimaryGeneratedColumn, Column, Index } = requireTypeorm()
|
||||
|
||||
@Entity()
|
||||
@Index(["providerCompleteApiUrl", "model", "providerApiSecret"])
|
||||
export class LlmModelUsageRecord {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
providerCompleteApiUrl: string
|
||||
|
||||
@Column()
|
||||
model: string
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
providerApiSecret: string
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
completionTokens?: number;
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
promptTokens?: number;
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
promptCacheHitTokens?: number
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
promptCacheMissTokens?: number
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
totalTokens?: number;
|
||||
|
||||
@Column()
|
||||
requestStartTime: Date
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
requestEndTime?: Date
|
||||
|
||||
@Column()
|
||||
hasError: boolean
|
||||
|
||||
@Column()
|
||||
errorMessage: string
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
requestScene?: number
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MarkAsNotSuitReason } from "../enums";
|
||||
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
import { ChatStartupFrom } from "./ChatStartupLog";
|
||||
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
|
||||
@@ -27,6 +27,11 @@ export class MarkAsNotSuitLog {
|
||||
})
|
||||
markReason?: MarkAsNotSuitReason
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
markOp?: MarkAsNotSuitOp
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
|
||||
@@ -3,4 +3,10 @@ export enum MarkAsNotSuitReason {
|
||||
BOSS_INACTIVE = 1,
|
||||
USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON = 2,
|
||||
JOB_NOT_SUIT = 3,
|
||||
}
|
||||
}
|
||||
|
||||
export enum MarkAsNotSuitOp {
|
||||
MARK_AS_NOT_SUIT_ON_BOSS = 1,
|
||||
MARK_AS_NOT_SUIT_ON_LOCAL = 2,
|
||||
NO_OP = 3
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataSource } from "typeorm";
|
||||
import { DataSource, Raw } from "typeorm";
|
||||
import { BossActiveStatusRecord } from "./entity/BossActiveStatusRecord";
|
||||
import { BossInfo } from "./entity/BossInfo";
|
||||
import { CompanyInfo } from "./entity/CompanyInfo";
|
||||
@@ -10,6 +10,7 @@ import { CompanyInfoChangeLog } from "./entity/CompanyInfoChangeLog";
|
||||
import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
|
||||
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
|
||||
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
|
||||
import { LlmModelUsageRecord } from "./entity/LlmModelUsageRecord";
|
||||
|
||||
function getBossInfoIfIsEqual (savedOne, currentOne) {
|
||||
if (savedOne === currentOne) {
|
||||
@@ -269,7 +270,7 @@ export async function saveMarkAsNotSuitRecord(
|
||||
ds: DataSource,
|
||||
_jobInfo,
|
||||
{ encryptUserId },
|
||||
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined } = {}
|
||||
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined } = {}
|
||||
) {
|
||||
const { jobInfo } = _jobInfo;
|
||||
|
||||
@@ -282,7 +283,8 @@ export async function saveMarkAsNotSuitRecord(
|
||||
autoStartupChatRecordId,
|
||||
markFrom,
|
||||
markReason,
|
||||
extInfo: extInfo ? JSON.stringify(extInfo) : undefined
|
||||
extInfo: extInfo ? JSON.stringify(extInfo) : undefined,
|
||||
markOp
|
||||
}
|
||||
Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload)
|
||||
|
||||
@@ -307,3 +309,33 @@ export async function saveChatMessageRecord(
|
||||
//#endregion
|
||||
return
|
||||
}
|
||||
|
||||
export async function saveGptCompletionRequestRecord(
|
||||
ds: DataSource,
|
||||
records: LlmModelUsageRecord[]
|
||||
) {
|
||||
//#region mark-as-not-suit-log
|
||||
const list = records.map(it => {
|
||||
const o = new LlmModelUsageRecord()
|
||||
for (const k of Object.keys(it)) {
|
||||
o[k] = it[k]
|
||||
}
|
||||
return o
|
||||
})
|
||||
const chatMessageRecordRepository = ds.getRepository(LlmModelUsageRecord);
|
||||
await chatMessageRecordRepository.save(list);
|
||||
//#endregion
|
||||
return
|
||||
}
|
||||
|
||||
export async function getNotSuitMarkRecordsInLastSomeDays (ds: DataSource, days = 0) {
|
||||
const repo = ds.getRepository(MarkAsNotSuitLog)
|
||||
const result = await repo.findBy({
|
||||
date: Raw(alias => `DATE(${alias}) >= DATE('${
|
||||
new Date(
|
||||
Number(new Date()) - 7 * 24 * 60 * 60 * 1000
|
||||
).toISOString()
|
||||
}')`)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "reflect-metadata";
|
||||
import { type DataSource } from "typeorm";
|
||||
import { requireTypeorm } from "./utils/module-loader";
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { BossInfo } from "./entity/BossInfo";
|
||||
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
|
||||
@@ -19,18 +20,22 @@ import { VJobLibrary } from "./entity/VJobLibrary";
|
||||
import { VCompanyLibrary } from "./entity/VCompanyLibrary"
|
||||
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
|
||||
import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
||||
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
||||
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord } from "./handlers";
|
||||
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord, getNotSuitMarkRecordsInLastSomeDays } from "./handlers";
|
||||
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
|
||||
import minimist from 'minimist'
|
||||
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
|
||||
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
|
||||
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
|
||||
import { Init1000000000000 } from "./migrations/1000000000000-Init";
|
||||
|
||||
export function initDb(dbFilePath) {
|
||||
const { DataSource } = requireTypeorm()
|
||||
const appDataSource = new DataSource({
|
||||
type: "sqlite",
|
||||
synchronize: true,
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: "simple-console",
|
||||
database: dbFilePath,
|
||||
@@ -53,10 +58,13 @@ export function initDb(dbFilePath) {
|
||||
MarkAsNotSuitLog,
|
||||
VMarkAsNotSuitLog,
|
||||
ChatMessageRecord,
|
||||
LlmModelUsageRecord,
|
||||
],
|
||||
migrations: [
|
||||
Init1000000000000,
|
||||
UpdateChatStartupLogTable1729182577167,
|
||||
UpdateBossInfoTable1732032381304
|
||||
UpdateBossInfoTable1732032381304,
|
||||
AddColumnForMarkAsNotSuitLog1746092370665,
|
||||
],
|
||||
migrationsRun: true
|
||||
});
|
||||
@@ -95,6 +103,48 @@ export default class SqlitePlugin {
|
||||
return await userInfoRepository.save(user);
|
||||
}
|
||||
);
|
||||
hooks.mainFlowWillLaunch.tapPromise(
|
||||
"SqlitePlugin",
|
||||
async ({
|
||||
jobNotMatchStrategy,
|
||||
jobNotActiveStrategy,
|
||||
blockJobNotSuit,
|
||||
blockBossNotActive,
|
||||
}) => {
|
||||
if (
|
||||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
|
||||
jobNotActiveStrategy === 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) {
|
||||
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) {
|
||||
last7DayMarkRecords
|
||||
.filter(it => it.markReason === MarkAsNotSuitReason.BOSS_INACTIVE)
|
||||
.map(
|
||||
it => it.encryptJobId
|
||||
)
|
||||
.forEach(
|
||||
id => blockJobNotSuit.add(id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async (_jobInfo) => {
|
||||
const ds = await this.initPromise;
|
||||
@@ -109,13 +159,14 @@ export default class SqlitePlugin {
|
||||
});
|
||||
});
|
||||
|
||||
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 } = {}) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, {
|
||||
autoStartupChatRecordId: this.runRecordId,
|
||||
markFrom,
|
||||
markReason,
|
||||
extInfo
|
||||
extInfo,
|
||||
markOp
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
24
packages/sqlite-plugin/src/migrations/1000000000000-Init.ts
Normal file
24
packages/sqlite-plugin/src/migrations/1000000000000-Init.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
export class Init1000000000000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const tableSchemaStatements = `CREATE TABLE IF NOT EXISTS "boss_info" ("encryptBossId" varchar PRIMARY KEY NOT NULL, "encryptCompanyId" varchar, "name" varchar NOT NULL, "date" datetime NOT NULL, "title" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "boss_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptBossId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "chat_startup_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "encryptCurrentUserId" varchar NOT NULL, "date" datetime NOT NULL, "chatStartupFrom" integer, "autoStartupChatRecordId" integer);
|
||||
CREATE TABLE IF NOT EXISTS "company_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptCompanyId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "company_info" ("encryptCompanyId" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "brandName" varchar NOT NULL, "scaleLow" integer, "scaleHigh" integer, "stageName" varchar, "industryName" varchar);
|
||||
CREATE TABLE IF NOT EXISTS "job_info" ("encryptJobId" varchar PRIMARY KEY NOT NULL, "jobName" varchar NOT NULL, "positionName" varchar NOT NULL, "salaryLow" integer, "salaryHigh" integer, "salaryMonth" integer, "experienceName" varchar NOT NULL, "publishDate" datetime, "degreeName" varchar, "address" varchar, "description" varchar NOT NULL, "encryptBossId" varchar NOT NULL, "encryptCompanyId" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "job_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "boss_active_status_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptBossId" varchar NOT NULL, "lastActiveStatus" varchar, "updateTime" datetime NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "user_info" ("encryptUserId" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "auto_start_chat_run_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "date" datetime NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "mark_as_not_suit_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "encryptCurrentUserId" varchar NOT NULL, "date" datetime NOT NULL, "markFrom" integer, "markReason" integer, "extInfo" varchar, "autoStartupChatRecordId" integer);
|
||||
CREATE TABLE IF NOT EXISTS "chat_message_record" ("mid" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptFromUserId" varchar NOT NULL, "encryptToUserId" varchar NOT NULL, "time" datetime, "type" varchar, "style" varchar, "text" varchar, "imageUrl" varchar, "imageWidth" integer, "imageHeight" integer);
|
||||
CREATE TABLE IF NOT EXISTS "llm_model_usage_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "providerCompleteApiUrl" varchar NOT NULL, "model" varchar NOT NULL, "providerApiSecret" varchar, "completionTokens" integer, "promptTokens" integer, "promptCacheHitTokens" integer, "promptCacheMissTokens" integer, "totalTokens" integer, "requestStartTime" datetime NOT NULL, "requestEndTime" datetime, "hasError" boolean NOT NULL, "errorMessage" varchar NOT NULL, "requestScene" integer);`.split('\n')
|
||||
for(const statement of tableSchemaStatements) {
|
||||
await queryRunner.query(statement);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { VBossLibrary } from "../entity/VBossLibrary";
|
||||
import { VChatStartupLog } from "../entity/VChatStartupLog";
|
||||
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
|
||||
import { VJobLibrary } from "../entity/VJobLibrary";
|
||||
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
|
||||
|
||||
const viewNames = [
|
||||
"v_boss_library",
|
||||
"v_chat_startup_log",
|
||||
"v_company_library",
|
||||
"v_job_library",
|
||||
"v_mark_as_not_suit_log"
|
||||
];
|
||||
const ViewEntities = [
|
||||
VBossLibrary,
|
||||
VChatStartupLog,
|
||||
VCompanyLibrary,
|
||||
VJobLibrary,
|
||||
VMarkAsNotSuitLog,
|
||||
]
|
||||
|
||||
export class UpdateChatStartupLogTable1729182577167
|
||||
implements MigrationInterface
|
||||
{
|
||||
export class UpdateChatStartupLogTable1729182577167 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const viewName of viewNames) {
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
|
||||
}
|
||||
if (await queryRunner.hasTable("boss_active_status_record")) {
|
||||
if (await queryRunner.hasColumn("boss_active_status_record", "updateDate")) {
|
||||
@@ -40,14 +45,17 @@ export class UpdateChatStartupLogTable1729182577167
|
||||
})
|
||||
)
|
||||
}
|
||||
for (const EntityDefinition of ViewEntities) {
|
||||
const dataSource = queryRunner.connection as DataSource;
|
||||
const viewMetadata = dataSource.getMetadata(EntityDefinition);
|
||||
let expression = viewMetadata.expression;
|
||||
if (typeof expression === 'function') {
|
||||
expression = expression(dataSource).getQuery();
|
||||
}
|
||||
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const viewName of viewNames) {
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
||||
}
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE boss_active_status_record RENAME COLUMN updateTime TO updateDate`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
import { VBossLibrary } from "../entity/VBossLibrary";
|
||||
import { VChatStartupLog } from "../entity/VChatStartupLog";
|
||||
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
|
||||
import { VJobLibrary } from "../entity/VJobLibrary";
|
||||
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
|
||||
|
||||
const viewNames = [
|
||||
"v_boss_library",
|
||||
"v_chat_startup_log",
|
||||
"v_company_library",
|
||||
"v_job_library",
|
||||
"v_mark_as_not_suit_log"
|
||||
];
|
||||
const ViewEntities = [
|
||||
VBossLibrary,
|
||||
VChatStartupLog,
|
||||
VCompanyLibrary,
|
||||
VJobLibrary,
|
||||
VMarkAsNotSuitLog,
|
||||
]
|
||||
|
||||
export class UpdateBossInfoTable1732032381304 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const viewName of viewNames) {
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
||||
}
|
||||
if (await queryRunner.hasTable("boss_info")) {
|
||||
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) {
|
||||
await queryRunner.changeColumn(
|
||||
'boss_info',
|
||||
'encryptCompanyId',
|
||||
new TableColumn({
|
||||
name: 'encryptCompanyId',
|
||||
type: 'varchar',
|
||||
isNullable: true
|
||||
})
|
||||
)
|
||||
debugger
|
||||
}
|
||||
}
|
||||
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}"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
if (await queryRunner.hasTable("boss_info")) {
|
||||
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) {
|
||||
await queryRunner.changeColumn(
|
||||
"boss_info",
|
||||
"encryptCompanyId",
|
||||
new TableColumn({
|
||||
name: "encryptCompanyId",
|
||||
type: "varchar",
|
||||
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,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> {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "geekgeekrun-ui",
|
||||
"version": "0.3.2",
|
||||
"version": "0.6.1",
|
||||
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "geekgeekrun",
|
||||
@@ -39,9 +39,12 @@
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"minimist": "^1.2.8",
|
||||
"mitt": "^3.0.1",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"pinia": "^3.0.2",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2"
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
@@ -72,6 +75,7 @@
|
||||
"normalize.css": "^8.0.1",
|
||||
"prettier": "^3.2.4",
|
||||
"sass": "^1.70.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "^5.3.3",
|
||||
"unocss": "^0.58.5",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "0.3.2",
|
||||
"buildVersion": 8,
|
||||
"buildTime": 1743435647129,
|
||||
"buildHash": "482ea006be25bc7014793c09cc748fae6606e27e",
|
||||
"version": "0.6.1",
|
||||
"buildVersion": 12,
|
||||
"buildTime": 1746891583817,
|
||||
"buildHash": "61c28ae1646c3cea900eae62acdd91b54a677508",
|
||||
"name": "geekgeekrun-ui"
|
||||
}
|
||||
1
packages/ui/src/common/constant.ts
Normal file
1
packages/ui/src/common/constant.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
|
||||
@@ -8,3 +8,13 @@ export enum AUTO_CHAT_ERROR_EXIT_CODE {
|
||||
AUTO_START_CHAT_DAEMON_PROCESS_SUICIDE = 86,
|
||||
AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87,
|
||||
}
|
||||
|
||||
export enum RECHAT_CONTENT_SOURCE {
|
||||
LOOK_FORWARD_EMOTION = 1,
|
||||
GEMINI_WITH_CHAT_CONTEXT = 2
|
||||
}
|
||||
|
||||
export enum RECHAT_LLM_FALLBACK {
|
||||
SEND_LOOK_FORWARD_EMOTION = 1,
|
||||
EXIT_REMINDER_PROGRAM = 2
|
||||
}
|
||||
|
||||
7
packages/ui/src/common/types/update.ts
Normal file
7
packages/ui/src/common/types/update.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface NewReleaseInfo {
|
||||
releaseVersion: string
|
||||
releasePageUrl: string
|
||||
assetUrl: string
|
||||
assetName: string
|
||||
releaseNote: string
|
||||
}
|
||||
9
packages/ui/src/common/utils/chat-list.ts
Normal file
9
packages/ui/src/common/utils/chat-list.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function messageForSaveFilter(it) {
|
||||
return (
|
||||
it.status !== 3 && // filter system notification out
|
||||
it.templateId === 1 && // filter system notification out
|
||||
((['text', 'sticker', 'image', 'sound', 'comDesc'].includes(it.messageType) &&
|
||||
!it.extend?.greetingQuestionAnswer) || // include those message, filter out auto ask
|
||||
(it.messageType === 'dialog' && [0, 1, 2, 8, 11, 12, 14, 17, 33].includes(it?.dialog?.type))) // include message like resume, phone, map, etc., filter out auto ask
|
||||
)
|
||||
}
|
||||
91
packages/ui/src/common/utils/resume.ts
Normal file
91
packages/ui/src/common/utils/resume.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export interface ResumeContent {
|
||||
name: string
|
||||
workYearDesc: string
|
||||
expectJob: string
|
||||
userDescription: string
|
||||
geekWorkExpList: Array<{
|
||||
company: string
|
||||
positionName: string
|
||||
startYearMon: string | null
|
||||
endYearMon: string | null
|
||||
performance: string
|
||||
workDescription: string
|
||||
}>
|
||||
geekProjExpList: Array<{
|
||||
name: string
|
||||
startYearMon: string
|
||||
endYearMon: string
|
||||
roleName: string
|
||||
projectDescription: string
|
||||
performance: string
|
||||
}>
|
||||
expectSalary: [string, string]
|
||||
}
|
||||
|
||||
export function formatResumeJsonToMarkdown(resume) {
|
||||
const basicInfoText = [
|
||||
['# 姓名', resume.content.name],
|
||||
['# 工作年限', resume.content.workYearDesc],
|
||||
['# 期望职位', resume.content.expectJob],
|
||||
['# 个人优势', resume.content.userDescription]
|
||||
]
|
||||
.filter((it) => {
|
||||
return Boolean(it[1]?.trim())
|
||||
})
|
||||
.map((it) => it.join('\n'))
|
||||
.join('\n\n')
|
||||
|
||||
let formattedWorkExpText = resume.content.geekWorkExpList
|
||||
.filter((it) => Boolean(it.company?.trim()))
|
||||
.map((it) => {
|
||||
const info = [
|
||||
[`职务`, it.positionName],
|
||||
[`任职时间`],
|
||||
[`工作描述`, it.workDescription],
|
||||
[`工作业绩`, it.performance]
|
||||
].filter((it) => {
|
||||
return Boolean(it[1]?.trim())
|
||||
})
|
||||
return [[`## ${it.company}`], ...info].map((it) => it.join('\n')).join('\n\n')
|
||||
})
|
||||
.join('\n')
|
||||
if (formattedWorkExpText?.trim()) {
|
||||
formattedWorkExpText = '# 工作经历\n' + formattedWorkExpText
|
||||
}
|
||||
|
||||
let formattedProjWorkExpText = resume.content.geekProjExpList
|
||||
.filter((it) => Boolean(it.name?.trim()))
|
||||
.map((it) => {
|
||||
const info = [
|
||||
[`## ${it.name}`],
|
||||
[`项目角色`, it.roleName],
|
||||
[`项目时间`],
|
||||
[`工作描述`, it.projectDescription],
|
||||
[`工作业绩`, it.performance]
|
||||
].filter((it) => {
|
||||
return Boolean(it[1]?.trim())
|
||||
})
|
||||
|
||||
return [[`## ${it.name}`], ...info].map((it) => it.join('\n')).join('\n\n')
|
||||
})
|
||||
.join('\n')
|
||||
if (formattedProjWorkExpText?.trim()) {
|
||||
formattedProjWorkExpText = '# 项目经历\n' + formattedProjWorkExpText
|
||||
}
|
||||
|
||||
const result = `${basicInfoText}\n\n${formattedWorkExpText}\n\n${formattedProjWorkExpText}`
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function checkIsResumeContentValid(resumeItem: { content: ResumeContent }) {
|
||||
return (
|
||||
!!resumeItem?.content &&
|
||||
resumeItem.content.geekProjExpList?.[0]?.name?.trim() &&
|
||||
resumeItem.content.geekWorkExpList?.[0]?.company?.trim()
|
||||
)
|
||||
}
|
||||
|
||||
export function resumeContentEnoughDetect(resumeItem: { content: ResumeContent }) {
|
||||
return resumeItem?.content && formatResumeJsonToMarkdown(resumeItem)?.length > 800
|
||||
}
|
||||
22
packages/ui/src/main/features/llm-request-log.ts
Normal file
22
packages/ui/src/main/features/llm-request-log.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { saveGptCompletionRequestRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
|
||||
export enum RequestSceneEnum {
|
||||
testing = 1,
|
||||
readNoReplyAutoReminder = 2,
|
||||
geekAutoStartChatWithBoss = 3
|
||||
}
|
||||
|
||||
let dbInitPromise
|
||||
export const recordGptCompletionRequest = async (payload) => {
|
||||
const { getPublicDbFilePath } = await import(
|
||||
'@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
)
|
||||
const { initDb } = await import('@geekgeekrun/sqlite-plugin')
|
||||
|
||||
if (!dbInitPromise) {
|
||||
dbInitPromise = initDb(getPublicDbFilePath())
|
||||
}
|
||||
const ds = await dbInitPromise
|
||||
const o = { ...payload }
|
||||
await saveGptCompletionRequestRecord(ds, [o])
|
||||
}
|
||||
103
packages/ui/src/main/features/updater.ts
Normal file
103
packages/ui/src/main/features/updater.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import semver from 'semver'
|
||||
import packageJson from '../../../package.json'
|
||||
import os from 'node:os'
|
||||
import gtag from '../utils/gtag'
|
||||
import { NewReleaseInfo } from '../../common/types/update'
|
||||
|
||||
export const currentOsPlatform = os.platform()
|
||||
const RELEASE_LIST_URL = `https://api.github.com/repos/geekgeekrun/geekgeekrun/releases`
|
||||
|
||||
export interface GitHubReleaseItem {
|
||||
tag_name: string
|
||||
prerelease: boolean
|
||||
draft: boolean
|
||||
html_url: string
|
||||
body: string
|
||||
assets: Array<{
|
||||
browser_download_url: string
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
|
||||
// find by arch, appendix
|
||||
function findTargetAsset(
|
||||
assets: GitHubReleaseItem['assets']
|
||||
): null | GitHubReleaseItem['assets'][number] {
|
||||
let assetsFilteredByPlatform: GitHubReleaseItem['assets'] = []
|
||||
switch (os.platform()) {
|
||||
case 'win32': {
|
||||
assetsFilteredByPlatform = assets.filter((it) => it.name.endsWith('.exe'))
|
||||
break
|
||||
}
|
||||
case 'darwin': {
|
||||
assetsFilteredByPlatform = assets.filter((it) => it.name.endsWith('.dmg'))
|
||||
break
|
||||
}
|
||||
case 'linux': {
|
||||
assetsFilteredByPlatform = assets.filter((it) => it.name.endsWith('.deb'))
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!assetsFilteredByPlatform?.length) {
|
||||
return null
|
||||
}
|
||||
if (assetsFilteredByPlatform.length === 1) {
|
||||
return assetsFilteredByPlatform[0]
|
||||
}
|
||||
let targetAsset
|
||||
if (os.platform() === 'darwin') {
|
||||
targetAsset = assetsFilteredByPlatform.find((it) => it.name.includes(`_${os.arch()}`))
|
||||
}
|
||||
return targetAsset
|
||||
}
|
||||
|
||||
export const checkUpdateForUi = async (): Promise<NewReleaseInfo | null> => {
|
||||
let releaseList: GitHubReleaseItem[] = []
|
||||
try {
|
||||
const headers = new Headers()
|
||||
headers.append('Accept', 'application/vnd.github+json')
|
||||
headers.append('X-GitHub-Api-Version', '2022-11-28')
|
||||
await fetch(RELEASE_LIST_URL, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
redirect: 'follow'
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (Array.isArray(res)) {
|
||||
releaseList = res
|
||||
} else {
|
||||
throw res
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
gtag('check_update_error', { err: JSON.stringify(err) })
|
||||
console.log(err)
|
||||
}
|
||||
console.log(releaseList)
|
||||
const availableRelease = releaseList.find((it) => !it.draft && !it.prerelease)
|
||||
if (!availableRelease) {
|
||||
return null
|
||||
}
|
||||
const availableReleaseVersion = availableRelease.tag_name.replace(/^(ui-v)/, '')
|
||||
if (!semver.gt(availableReleaseVersion, packageJson.version)) {
|
||||
return null
|
||||
}
|
||||
const { assets } = availableRelease
|
||||
const targetAsset = findTargetAsset(assets ?? [])
|
||||
if (targetAsset) {
|
||||
gtag('update_found', {
|
||||
currentVersion: packageJson.version,
|
||||
newVersion: availableReleaseVersion
|
||||
})
|
||||
console.log(targetAsset)
|
||||
return {
|
||||
releaseVersion: availableReleaseVersion,
|
||||
releasePageUrl: availableRelease.html_url,
|
||||
assetUrl: targetAsset.browser_download_url,
|
||||
assetName: targetAsset.name,
|
||||
releaseNote: availableRelease.body
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -86,6 +86,7 @@ const runAutoChat = async () => {
|
||||
pageLoaded: new SyncHook(),
|
||||
cookieWillSet: new SyncHook(['cookies']),
|
||||
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
||||
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
|
||||
jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']),
|
||||
newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']),
|
||||
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { app } from 'electron'
|
||||
import { initPuppeteer } from '@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
|
||||
import extractZip from 'extract-zip'
|
||||
import {
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
@@ -17,10 +16,6 @@ import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss
|
||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import url from 'url'
|
||||
import packageJson from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension/package.json' assert { type: 'json' }
|
||||
import { Target } from 'puppeteer'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
@@ -29,39 +24,13 @@ import gtag from '../../utils/gtag'
|
||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||
import { type ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/src/entity/ChatMessageRecord'
|
||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
import {
|
||||
ensureEditThisCookie,
|
||||
editThisCookieExtensionPath
|
||||
} from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension/utils.mjs'
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
const extensionDir = path.join(runtimeFolderPath, 'chrome-extensions')
|
||||
if (!fs.existsSync(runtimeFolderPath)) {
|
||||
fs.mkdirSync(runtimeFolderPath)
|
||||
}
|
||||
if (!fs.existsSync(extensionDir)) {
|
||||
fs.mkdirSync(extensionDir)
|
||||
}
|
||||
const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
|
||||
|
||||
let editThisCookieZipPath
|
||||
async function getEditThisCookieZipPath() {
|
||||
if (editThisCookieZipPath) {
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
if (isRunFromUi) {
|
||||
const { app } = await import('electron')
|
||||
editThisCookieZipPath = path.join(
|
||||
app.getAppPath(),
|
||||
'./node_modules',
|
||||
packageJson.name,
|
||||
'extensions',
|
||||
'EditThisCookie.zip'
|
||||
)
|
||||
} else {
|
||||
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
|
||||
}
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||
|
||||
const attachRequestsListener = async (target: Target) => {
|
||||
@@ -90,7 +59,7 @@ const attachRequestsListener = async (target: Target) => {
|
||||
}
|
||||
await writeStorageFile('job-not-suit-reason-code-to-text-cache.json', reasonCodeToTextMap)
|
||||
} else if (
|
||||
page.url().startsWith('https://www.zhipin.com/web/geek/job-recommend') &&
|
||||
page.url().startsWith('https://www.zhipin.com/web/geek/jobs') &&
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/save.json')
|
||||
) {
|
||||
const currentJobData = await page.evaluate(
|
||||
@@ -137,7 +106,7 @@ const attachRequestsListener = async (target: Target) => {
|
||||
markDetail
|
||||
)
|
||||
} else if (
|
||||
page.url().startsWith('https://www.zhipin.com/web/geek/job-recommend') &&
|
||||
page.url().startsWith('https://www.zhipin.com/web/geek/jobs') &&
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/friend/add.json')
|
||||
) {
|
||||
const request = response.request().url()
|
||||
@@ -213,23 +182,14 @@ const attachRequestsListener = async (target: Target) => {
|
||||
await page.evaluate(
|
||||
'document.querySelector(".message-content .chat-record").__vue__.list$'
|
||||
)
|
||||
)?.filter((it) => {
|
||||
return (
|
||||
it.status !== 3 && // filter system notification out
|
||||
it.templateId === 1 && // filter system notification out
|
||||
(
|
||||
(['text', 'sticker', 'image', 'sound', 'comDesc'].includes(it.messageType) && !it.extend?.greetingQuestionAnswer) // include those message, filter out auto ask
|
||||
|| (it.messageType === 'dialog' && [0, 1, 2, 8, 11, 12, 14, 17, 33].includes(it?.dialog?.type)) // include message like resume, phone, map, etc., filter out auto ask
|
||||
)
|
||||
)
|
||||
}) ?? []
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const chatRecordList = rawChatRecordList.map(it => {
|
||||
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
|
||||
mappedItem.mid = it.mid
|
||||
mappedItem.encryptFromUserId = it.isSelf ? currentUserInfo.encryptUserId : bossInfo.encryptBossId
|
||||
mappedItem.encryptToUserId = it.isSelf ? bossInfo.encryptBossId : currentUserInfo.encryptUserId
|
||||
mappedItem.style = it.style
|
||||
mappedItem.style = it.isSelf ? 'sent' : 'received'
|
||||
mappedItem.type = it.type
|
||||
mappedItem.time = it.time ? new Date(it.time) : null
|
||||
mappedItem.text = it.text
|
||||
@@ -255,12 +215,7 @@ const attachRequestsListener = async (target: Target) => {
|
||||
|
||||
export async function launchBossSite() {
|
||||
app.dock?.hide()
|
||||
if (!fs.existsSync(path.join(editThisCookieExtensionPath, 'manifest.json'))) {
|
||||
await extractZip(await getEditThisCookieZipPath(), {
|
||||
dir: extensionDir
|
||||
})
|
||||
}
|
||||
|
||||
await ensureEditThisCookie()
|
||||
const bossCookies = readStorageFile('boss-cookies.json')
|
||||
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createMainWindow } from '../../window/mainWindow'
|
||||
import './app-menu'
|
||||
import initIpc from './ipc'
|
||||
import gtag from '../../utils/gtag'
|
||||
import initPublicIpc from '../../utils/initPublicIpc'
|
||||
export function openSettingWindow() {
|
||||
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
@@ -31,6 +32,7 @@ export function openSettingWindow() {
|
||||
|
||||
// IPC test
|
||||
ipcMain.on('ping', () => console.log('pong'))
|
||||
initPublicIpc()
|
||||
initIpc()
|
||||
|
||||
app.on('activate', function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ipcMain, shell, app } from 'electron'
|
||||
|
||||
import path from 'path'
|
||||
import * as childProcess from 'node:child_process'
|
||||
import {
|
||||
ensureConfigFileExist,
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
readConfigFile,
|
||||
writeConfigFile,
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
writeStorageFile,
|
||||
storageFilePath
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { ChildProcess } from 'child_process'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
@@ -30,14 +31,29 @@ import { pipeWriteRegardlessError } from '../../utils/pipe'
|
||||
import { WriteStream } from 'node:fs'
|
||||
// eslint-disable-next-line vue/prefer-import-from-vue
|
||||
import { hasOwn } from '@vue/shared'
|
||||
import { createLlmConfigWindow, llmConfigWindow } from '../../../window/llmConfigWindow'
|
||||
import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/resumeEditorWindow'
|
||||
import {
|
||||
getValidTemplate,
|
||||
requestNewMessageContent
|
||||
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
|
||||
import {
|
||||
autoReminderPromptTemplateFileName,
|
||||
writeDefaultAutoRemindPrompt
|
||||
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
|
||||
import {
|
||||
checkIsResumeContentValid,
|
||||
resumeContentEnoughDetect
|
||||
} from '../../../../common/utils/resume'
|
||||
import {
|
||||
createReadNoReplyReminderLlmMockWindow,
|
||||
readNoReplyReminderLlmMockWindow
|
||||
} from '../../../window/readNoReplyReminderLlmMockWindow'
|
||||
import { RequestSceneEnum } from '../../../features/llm-request-log'
|
||||
import { checkUpdateForUi } from '../../../features/updater'
|
||||
import gtag from '../../../utils/gtag'
|
||||
|
||||
export default function initIpc() {
|
||||
ipcMain.on('open-external-link', (_, link) => {
|
||||
shell.openExternal(link, {
|
||||
activate: true
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('fetch-config-file-content', async () => {
|
||||
const configFileContentList = configFileNameList.map((fileName) => {
|
||||
return readConfigFile(fileName)
|
||||
@@ -69,8 +85,24 @@ export default function initIpc() {
|
||||
if (hasOwn(payload, 'anyCombineRecommendJobFilter')) {
|
||||
bossConfig.anyCombineRecommendJobFilter = payload.anyCombineRecommendJobFilter
|
||||
}
|
||||
if (hasOwn(payload, 'expectJobRegExpStr')) {
|
||||
bossConfig.expectJobRegExpStr = payload.expectJobRegExpStr
|
||||
delete bossConfig.expectJobRegExpStr
|
||||
if (hasOwn(payload, 'expectJobNameRegExpStr')) {
|
||||
bossConfig.expectJobNameRegExpStr = payload.expectJobNameRegExpStr
|
||||
}
|
||||
if (hasOwn(payload, 'expectJobTypeRegExpStr')) {
|
||||
bossConfig.expectJobTypeRegExpStr = payload.expectJobTypeRegExpStr
|
||||
}
|
||||
if (hasOwn(payload, 'expectJobDescRegExpStr')) {
|
||||
bossConfig.expectJobDescRegExpStr = payload.expectJobDescRegExpStr
|
||||
}
|
||||
if (hasOwn(payload, 'jobNotMatchStrategy')) {
|
||||
bossConfig.jobNotMatchStrategy = payload.jobNotMatchStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'markAsNotActiveSelectedTimeRange')) {
|
||||
bossConfig.markAsNotActiveSelectedTimeRange = payload.markAsNotActiveSelectedTimeRange
|
||||
}
|
||||
if (hasOwn(payload, 'jobNotActiveStrategy')) {
|
||||
bossConfig.jobNotActiveStrategy = payload.jobNotActiveStrategy
|
||||
}
|
||||
if (hasOwn(payload, 'autoReminder')) {
|
||||
bossConfig.autoReminder = payload.autoReminder
|
||||
@@ -435,6 +467,134 @@ export default function initIpc() {
|
||||
return await getJobHistoryByEncryptId(encryptJobId)
|
||||
})
|
||||
|
||||
ipcMain.handle('llm-config', async () => {
|
||||
createLlmConfigWindow({
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
})
|
||||
const defer = Promise.withResolvers()
|
||||
async function saveLlmConfigHandler(_, configToSave) {
|
||||
await writeConfigFile('llm.json', configToSave)
|
||||
defer.resolve()
|
||||
ipcMain.removeHandler('save-llm-config')
|
||||
llmConfigWindow?.close()
|
||||
}
|
||||
ipcMain.handle('save-llm-config', saveLlmConfigHandler)
|
||||
llmConfigWindow?.once('closed', () => {
|
||||
ipcMain.removeHandler('save-llm-config')
|
||||
defer.reject(new Error('cancel'))
|
||||
})
|
||||
return defer.promise
|
||||
})
|
||||
ipcMain.on('close-llm-config', () => llmConfigWindow?.close())
|
||||
|
||||
ipcMain.handle('resume-edit', async () => {
|
||||
createResumeEditorWindow({
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
})
|
||||
const defer = Promise.withResolvers()
|
||||
async function saveResumeHandler(_, resumeContent) {
|
||||
await writeConfigFile('resumes.json', [
|
||||
{
|
||||
name: '默认简历',
|
||||
updateTime: Number(new Date()),
|
||||
content: resumeContent
|
||||
}
|
||||
])
|
||||
defer.resolve()
|
||||
resumeEditorWindow?.close()
|
||||
}
|
||||
ipcMain.handle('save-resume-content', saveResumeHandler)
|
||||
resumeEditorWindow?.once('closed', () => {
|
||||
ipcMain.removeHandler('save-resume-content')
|
||||
defer.reject(new Error('cancel'))
|
||||
})
|
||||
|
||||
return defer.promise
|
||||
})
|
||||
ipcMain.handle('fetch-resume-content', async () => {
|
||||
const res = (await readConfigFile('resumes.json'))?.[0]
|
||||
return res?.content ?? null
|
||||
})
|
||||
ipcMain.on('no-reply-reminder-prompt-edit', async () => {
|
||||
const template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false })
|
||||
if (!template) {
|
||||
await writeDefaultAutoRemindPrompt()
|
||||
}
|
||||
const filePath = path.join(storageFilePath, autoReminderPromptTemplateFileName)
|
||||
shell.openPath(filePath)
|
||||
})
|
||||
ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close())
|
||||
ipcMain.handle('check-if-auto-remind-prompt-valid', async () => {
|
||||
await getValidTemplate()
|
||||
})
|
||||
ipcMain.handle('check-is-resume-content-valid', async () => {
|
||||
const res = (await readConfigFile('resumes.json'))?.[0]
|
||||
return checkIsResumeContentValid(res)
|
||||
})
|
||||
ipcMain.handle('resume-content-enough-detect', async () => {
|
||||
const res = (await readConfigFile('resumes.json'))?.[0]
|
||||
return resumeContentEnoughDetect(res)
|
||||
})
|
||||
ipcMain.handle('overwrite-auto-remind-prompt-with-default', async () => {
|
||||
await writeDefaultAutoRemindPrompt()
|
||||
})
|
||||
ipcMain.handle('check-if-llm-config-list-valid', async () => {
|
||||
const llmConfigList = await readConfigFile('llm.json')
|
||||
if (!Array.isArray(llmConfigList) || !llmConfigList?.length) {
|
||||
throw new Error('CANNOT_FIND_VALID_CONFIG')
|
||||
}
|
||||
if (llmConfigList.some((it) => !/^http(s)?:\/\//.test(it.providerCompleteApiUrl))) {
|
||||
throw new Error('CANNOT_FIND_VALID_CONFIG')
|
||||
}
|
||||
if (llmConfigList.length > 1) {
|
||||
const firstEnabledModel = llmConfigList.find((it) => it.enabled)
|
||||
if (!firstEnabledModel) {
|
||||
throw new Error('CANNOT_FIND_VALID_CONFIG')
|
||||
}
|
||||
}
|
||||
})
|
||||
ipcMain.on('test-llm-config-effect', (_, { autoReminderConfig } = {}) => {
|
||||
createReadNoReplyReminderLlmMockWindow(
|
||||
{
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
autoReminderConfig
|
||||
}
|
||||
)
|
||||
async function requestLlm(_, requestPayload) {
|
||||
return await requestNewMessageContent(requestPayload.messageList, {
|
||||
requestScene: RequestSceneEnum.testing,
|
||||
llmConfigIdForPick: requestPayload.llmConfigIdForPick ?? null
|
||||
})
|
||||
}
|
||||
ipcMain.handle('request-llm-for-test', requestLlm)
|
||||
readNoReplyReminderLlmMockWindow?.once('closed', () => {
|
||||
ipcMain.removeHandler('request-llm-for-test')
|
||||
})
|
||||
async function getLlmConfigList() {
|
||||
return await readConfigFile('llm.json')
|
||||
}
|
||||
ipcMain.handle('get-llm-config-for-test', getLlmConfigList)
|
||||
readNoReplyReminderLlmMockWindow?.once('closed', () => {
|
||||
ipcMain.removeHandler('get-llm-config-for-test')
|
||||
})
|
||||
})
|
||||
ipcMain.on('close-read-no-reply-reminder-llm-mock-window', () => {
|
||||
readNoReplyReminderLlmMockWindow?.close()
|
||||
gtag('mock_chat_window_closed')
|
||||
})
|
||||
ipcMain.handle('check-update', async () => {
|
||||
const newRelease = await checkUpdateForUi()
|
||||
return newRelease
|
||||
})
|
||||
|
||||
ipcMain.handle('exit-app-immediately', () => {
|
||||
app.exit(0)
|
||||
})
|
||||
|
||||
@@ -47,7 +47,10 @@ const payloadHandler = {
|
||||
const [data, totalItemCount] = await measureExecutionTime(
|
||||
userRepository.findAndCount({
|
||||
skip: (pageNo - 1) * pageSize,
|
||||
take: pageSize
|
||||
take: pageSize,
|
||||
order: {
|
||||
date: 'DESC'
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
@@ -69,7 +72,10 @@ const payloadHandler = {
|
||||
const [data, totalItemCount] = await measureExecutionTime(
|
||||
recordRepository.findAndCount({
|
||||
skip: (pageNo - 1) * pageSize,
|
||||
take: pageSize
|
||||
take: pageSize,
|
||||
order: {
|
||||
date: 'DESC'
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { sleepWithRandomDelay, sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { completes } from '@geekgeekrun/utils/gpt-request.mjs'
|
||||
import { recordGptCompletionRequest, RequestSceneEnum } from '../../features/llm-request-log'
|
||||
import {
|
||||
readConfigFile,
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { formatResumeJsonToMarkdown } from '../../../common/utils/resume'
|
||||
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../common/constant'
|
||||
import { LlmModelUsageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/LlmModelUsageRecord'
|
||||
import gtag from '../../utils/gtag'
|
||||
|
||||
export const sendLookForwardReplyEmotion = async (page: Page) => {
|
||||
const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion')
|
||||
@@ -15,3 +26,241 @@ export const sendLookForwardReplyEmotion = async (page: Page) => {
|
||||
)
|
||||
await lookForwardReplyEmojiProxy!.click()
|
||||
}
|
||||
|
||||
const pickLlmConfigFromList = (llmConfigList, blockModelSet) => {
|
||||
if (llmConfigList.length === 1) {
|
||||
llmConfigList[0].enabled = true
|
||||
llmConfigList[0].serveWeight = SINGLE_ITEM_DEFAULT_SERVE_WEIGHT
|
||||
}
|
||||
llmConfigList = llmConfigList.filter((it) => it.enabled && !blockModelSet.has(it.id))
|
||||
if (!llmConfigList.length) {
|
||||
return null
|
||||
}
|
||||
llmConfigList.forEach((conf) => {
|
||||
if (!Number(conf.serveWeight) || conf.serveWeight < 1) {
|
||||
conf.serveWeight = 1
|
||||
}
|
||||
if (conf.serveWeight > 100) {
|
||||
conf.serveWeight = 100
|
||||
}
|
||||
})
|
||||
const pool: number[] = []
|
||||
for (let i = 0; i < llmConfigList.length; i++) {
|
||||
for (let j = 0; j < Math.floor(llmConfigList[i].serveWeight); j++) {
|
||||
pool.push(llmConfigList[i].id)
|
||||
}
|
||||
}
|
||||
if (!pool.length) {
|
||||
return null
|
||||
}
|
||||
const index = Math.floor(pool.length * Math.random())
|
||||
return llmConfigList.find((it) => it.id === pool[index]) ?? null
|
||||
}
|
||||
|
||||
// let _index = 0
|
||||
|
||||
const RESUME_PLACEHOLDER = `__REPLACE_REAL_RESUME_HERE__`
|
||||
const defaultPrompt = `**核心指令:**
|
||||
你是一个智能求职助手,需要根据用户简历生成30字左右的提醒消息,满足以下要求:
|
||||
1. 每次生成需满足:
|
||||
- √ 包含1个核心技能 + 1个成果量化
|
||||
- √ 使用不同句式模板(至少准备5种)
|
||||
- √ 谦虚一些,头衔、工作年限等在历史记录信息中出现一次就好
|
||||
- ✗ 严禁与最近发送的几条相似或雷同
|
||||
- ✗ 严禁出现简历之外的词语
|
||||
- ✗ 严禁包含最近8条已经发过的内容(包括但不限于职位名称)
|
||||
|
||||
**简历分析层:**
|
||||
请从以下简历内容中提取关键要素:\n\`\`\`markdown\n${RESUME_PLACEHOLDER}\n\`\`\`\n
|
||||
|
||||
---
|
||||
要求提取:
|
||||
1. 硬技能:编程语言/技术栈/工具证书等(至少提取5项)
|
||||
2. 项目经历与成果:业绩、带量化数据的结果(至少3条)
|
||||
3. 软技能:沟通/管理等(至少2项)
|
||||
4. 特殊成就:奖项/专利等(可选)
|
||||
|
||||
**消息生成层:**
|
||||
根据上述要素随机组合生成消息
|
||||
|
||||
**质量控制层:**
|
||||
每次生成前执行:
|
||||
1. 检查历史记录
|
||||
2. 确保技能/成果组合未重复
|
||||
3. 确保所生成的新消息不包含最近8条已经发过的内容(包括但不限于职位名称)
|
||||
4. 字数严格控制在10-40字
|
||||
5. 避免感叹号等激进符号
|
||||
6. 减少头衔“资深”、“高级”出现的频率,严禁出现“专家”、“老兵”;减少工作年限“x年”出现的频率
|
||||
|
||||
**输出格式:**
|
||||
请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:\`{"response": "这里是将会发送给招聘者的内容"}\``
|
||||
|
||||
export const autoReminderPromptTemplateFileName = 'auto-reminder-resume-system-message-template.md'
|
||||
export const getValidTemplate = async () => {
|
||||
let template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false })
|
||||
if (!template) {
|
||||
await writeDefaultAutoRemindPrompt()
|
||||
template = defaultPrompt
|
||||
}
|
||||
if (!template.includes(RESUME_PLACEHOLDER)) {
|
||||
const e = new Error(`简历内容占位符字符串不存在。占位字符串是 ${RESUME_PLACEHOLDER}`)
|
||||
e.name = `RESUME_PLACEHOLDER_NOT_EXIST`
|
||||
throw e
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
export const writeDefaultAutoRemindPrompt = async () => {
|
||||
await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false })
|
||||
}
|
||||
|
||||
export const requestNewMessageContent = async (
|
||||
chatRecords,
|
||||
{
|
||||
requestScene,
|
||||
llmConfigIdForPick
|
||||
}: { requestScene?: RequestSceneEnum; llmConfigIdForPick?: string[] } = {}
|
||||
) => {
|
||||
const template = await getValidTemplate()
|
||||
const resumeObject = (await readConfigFile('resumes.json'))?.[0]
|
||||
const resumeContent = formatResumeJsonToMarkdown(resumeObject)
|
||||
const chatList = [
|
||||
{
|
||||
role: 'system',
|
||||
content: template.replace(RESUME_PLACEHOLDER, resumeContent)
|
||||
}
|
||||
]
|
||||
chatList.push({
|
||||
role: 'user',
|
||||
content:
|
||||
'请根据我的简历,帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容,但需要表达出应聘意向。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`'
|
||||
})
|
||||
// chatRecords = chatRecords.slice(chatRecords.length - _index)
|
||||
// debugger
|
||||
for (const record of chatRecords) {
|
||||
const assistantJsonContent = JSON.stringify({
|
||||
response: record.text
|
||||
})
|
||||
chatList.push({
|
||||
role: 'assistant',
|
||||
content: `\`\`\`json\n${assistantJsonContent}\n\`\`\``
|
||||
})
|
||||
chatList.push({
|
||||
role: 'user',
|
||||
content:
|
||||
'围绕我简历中关于自我介绍、技术栈、工作经历、项目描述、项目业绩等内容,写一句自我介绍。开头不必包含“您好”、结尾不必包含“期待回复”;务必确保本次所回复的内容不能与之前所回复的内容雷同或相似。请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`'
|
||||
})
|
||||
}
|
||||
console.log(chatList)
|
||||
let res, llmConfig
|
||||
const llmRequestRecord: Omit<LlmModelUsageRecord, 'id' | 'providerApiSecretMd5'> & {
|
||||
providerApiSecret: string
|
||||
} = {}
|
||||
const blockModelSet = new Set()
|
||||
while (!res) {
|
||||
let llmConfigList = await readConfigFile('llm.json')
|
||||
if (llmConfigIdForPick?.length) {
|
||||
llmConfigList = llmConfigList.filter((it) => {
|
||||
return llmConfigIdForPick.includes(it.id)
|
||||
})
|
||||
}
|
||||
llmConfig = pickLlmConfigFromList(llmConfigList, blockModelSet)
|
||||
if (!llmConfig) {
|
||||
throw new Error(`CANNOT_FIND_A_USABLE_MODEL`)
|
||||
}
|
||||
console.log(llmConfig.providerCompleteApiUrl)
|
||||
Object.assign(llmRequestRecord, {
|
||||
providerCompleteApiUrl: llmConfig.providerCompleteApiUrl,
|
||||
model: llmConfig.model,
|
||||
providerApiSecret: llmConfig.providerApiSecret,
|
||||
requestStartTime: new Date(),
|
||||
hasError: false,
|
||||
errorMessage: '',
|
||||
requestScene
|
||||
})
|
||||
try {
|
||||
const completion = await completes(
|
||||
{
|
||||
baseURL: llmConfig.providerCompleteApiUrl,
|
||||
apiKey: llmConfig.providerApiSecret,
|
||||
model: llmConfig.model
|
||||
},
|
||||
chatList
|
||||
)
|
||||
res = completion?.choices?.[0] ?? null
|
||||
Object.assign(llmRequestRecord, {
|
||||
completionTokens: completion.usage?.completion_tokens ?? null,
|
||||
promptCacheHitTokens: completion.usage?.prompt_cache_hit_tokens ?? null,
|
||||
promptCacheMissTokens: completion.usage?.prompt_cache_miss_tokens ?? null,
|
||||
promptTokens: completion.usage?.prompt_tokens ?? null,
|
||||
totalTokens: completion.usage?.total_tokens ?? null
|
||||
} as LlmModelUsageRecord)
|
||||
} catch (err) {
|
||||
console.log('request failed', err)
|
||||
blockModelSet.add(llmConfig.id)
|
||||
Object.assign(llmRequestRecord, {
|
||||
hasError: true,
|
||||
errorMessage: err?.message ?? ''
|
||||
})
|
||||
} finally {
|
||||
llmRequestRecord.requestEndTime = new Date()
|
||||
try {
|
||||
await recordGptCompletionRequest(llmRequestRecord)
|
||||
} catch (err) {
|
||||
console.log('CANNOT_SAVE_LLM_COMPLETION_LOG', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(res)
|
||||
// _index++
|
||||
let textToSend
|
||||
try {
|
||||
const rawMarkdownText = res?.message?.content
|
||||
try {
|
||||
textToSend = JSON.parse(
|
||||
rawMarkdownText.replace(/^```json/m, '').replace(/```$/m, '')
|
||||
)?.response
|
||||
} catch (err) {
|
||||
gtag('encounter_error_when_parse_llm_text', {
|
||||
err,
|
||||
model: llmConfig?.model,
|
||||
providerCompleteApiUrl: llmConfig?.providerCompleteApiUrl
|
||||
})
|
||||
throw err
|
||||
}
|
||||
textToSend = textToSend?.replace(/。$/, '')
|
||||
if (!textToSend) {
|
||||
gtag('llm_respond_text_is_empty', {
|
||||
model: llmConfig?.model,
|
||||
providerCompleteApiUrl: llmConfig?.providerCompleteApiUrl
|
||||
})
|
||||
throw new Error(`empty content. ${err?.message} ${res?.message?.content}`)
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`fail to parse response. ${err?.message} ${res?.message?.content}`)
|
||||
}
|
||||
return {
|
||||
responseText: textToSend,
|
||||
usedLlmConfig: llmConfig,
|
||||
recordInfo: llmRequestRecord
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendGptContent(page: Page, chatRecords) {
|
||||
const textToSend = (
|
||||
await requestNewMessageContent(chatRecords, {
|
||||
requestScene: RequestSceneEnum.readNoReplyAutoReminder
|
||||
})
|
||||
).responseText
|
||||
const chatInputSelector = `.chat-conversation .message-controls .chat-input`
|
||||
const chatInputHandle = await page.$(chatInputSelector)
|
||||
await chatInputHandle.click()
|
||||
await sleep(500)
|
||||
await chatInputHandle.click()
|
||||
await chatInputHandle.type(textToSend, {
|
||||
delay: 50
|
||||
})
|
||||
await sleep(1000)
|
||||
const sendButtonSelector = `.chat-conversation .message-controls .chat-op .btn-send:not(.disabled)`
|
||||
await page.click(sendButtonSelector)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { bootstrap, launchBoss } from './bootstrap'
|
||||
import { MsgStatus, type ChatListItem } from './types'
|
||||
import { Browser, Page } from 'puppeteer'
|
||||
import { sendLookForwardReplyEmotion } from './boss-operation'
|
||||
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
|
||||
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||
import { app } from 'electron'
|
||||
import { app, dialog } from 'electron'
|
||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||
import {
|
||||
getPublicDbFilePath,
|
||||
@@ -16,10 +16,26 @@ import { writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/ru
|
||||
import * as fs from 'fs'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||
import { RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat'
|
||||
import gtag from '../../utils/gtag'
|
||||
|
||||
const throttleIntervalMinutes =
|
||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||
const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21
|
||||
const recentMessageQuantityForLlm =
|
||||
readConfigFile('boss.json').autoReminder?.recentMessageQuantityForLlm ?? 8
|
||||
const rechatContentSource =
|
||||
readConfigFile('boss.json').autoReminder?.rechatContentSource ??
|
||||
RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION
|
||||
const rechatLlmFallback =
|
||||
readConfigFile('boss.json').autoReminder?.rechatLlmFallback ??
|
||||
RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||
|
||||
const expectJobTypeRegExpStr = readConfigFile('boss.json').expectJobTypeRegExpStr
|
||||
const onlyRemindBossWithExpectJobType =
|
||||
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithExpectJobType ?? !!expectJobTypeRegExpStr
|
||||
|
||||
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||
|
||||
export const pageMapByName: {
|
||||
@@ -105,7 +121,6 @@ const mainLoop = async () => {
|
||||
'你与该职位竞争者PK情况',
|
||||
'简历诊断提醒',
|
||||
'附件简历还没准备好',
|
||||
'开场问题,期待你的回答',
|
||||
'设置合适的期望薪资范围'
|
||||
].map((it) => new RegExp(it))
|
||||
browser = await bootstrap()
|
||||
@@ -171,6 +186,9 @@ const mainLoop = async () => {
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await pageMapByName.boss?.waitForFunction(() => {
|
||||
return Array.isArray(document.querySelector('.main-wrap .chat-user')?.__vue__?.list)
|
||||
})
|
||||
// find target boss - with unread icon, or recommend system message
|
||||
const friendListData = (await pageMapByName.boss!.evaluate(
|
||||
`
|
||||
@@ -183,9 +201,10 @@ const mainLoop = async () => {
|
||||
(rechatLimitDay && it.updateTime
|
||||
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
|
||||
: true) &&
|
||||
((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) ||
|
||||
((((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) ||
|
||||
canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) &&
|
||||
!it.unreadCount
|
||||
!it.unreadCount) ||
|
||||
(!it.lastIsSelf && it.lastText === '开场问题,期待你的回答'))
|
||||
)
|
||||
})
|
||||
|
||||
@@ -252,8 +271,26 @@ const mainLoop = async () => {
|
||||
})
|
||||
}
|
||||
await sleepWithRandomDelay(1500)
|
||||
const bossInfo = await pageMapByName.boss?.evaluate(
|
||||
'document.querySelector(".chat-conversation .chat-record")?.__vue__?.boss'
|
||||
// check if expect job type match
|
||||
let isExpectJobTypeMatch = true
|
||||
if (onlyRemindBossWithExpectJobType) {
|
||||
const selectedFriendInfo = await pageMapByName.boss?.evaluate(
|
||||
`document.querySelector('.chat-conversation')?.__vue__?.selectedFriend$`
|
||||
)
|
||||
if (!selectedFriendInfo) {
|
||||
isExpectJobTypeMatch = false
|
||||
} else {
|
||||
const jobType = selectedFriendInfo?.positionName
|
||||
if (!jobType) {
|
||||
isExpectJobTypeMatch = false
|
||||
} else {
|
||||
const regExp = new RegExp(expectJobTypeRegExpStr)
|
||||
isExpectJobTypeMatch = regExp.test(jobType)
|
||||
}
|
||||
}
|
||||
}
|
||||
const conversationInfo = await pageMapByName.boss?.evaluate(
|
||||
`document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$`
|
||||
)
|
||||
|
||||
const historyMessageList =
|
||||
@@ -261,22 +298,16 @@ const mainLoop = async () => {
|
||||
await pageMapByName.boss?.evaluate(() => {
|
||||
return document.querySelector('.message-content .chat-record')?.__vue__?.list$ ?? []
|
||||
})
|
||||
)?.filter((it) => {
|
||||
return (
|
||||
it.status !== 3 && // filter system notification out
|
||||
it.templateId === 1 && // filter system notification out
|
||||
((['text', 'sticker', 'image', 'sound', 'comDesc'].includes(it.messageType) &&
|
||||
!it.extend?.greetingQuestionAnswer) || // include those message, filter out auto ask
|
||||
(it.messageType === 'dialog' &&
|
||||
[0, 1, 2, 8, 11, 12, 14, 17, 33].includes(it?.dialog?.type))) // include message like resume, phone, map, etc., filter out auto ask
|
||||
)
|
||||
}) ?? []
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0
|
||||
if (
|
||||
isExpectJobTypeMatch &&
|
||||
historyMessageList[historyMessageList.length - 1].isSelf &&
|
||||
historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ &&
|
||||
((bossInfo && !bossInfo.bothTalked) ||
|
||||
((conversationInfo &&
|
||||
Object.hasOwn(conversationInfo, 'bothTalked') &&
|
||||
!conversationInfo.bothTalked) ||
|
||||
!historyMessageList.filter(
|
||||
(it) => !it.isSelf // not sent by me
|
||||
).length) &&
|
||||
@@ -285,7 +316,31 @@ const mainLoop = async () => {
|
||||
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
|
||||
) {
|
||||
await sleepWithRandomDelay(3250)
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
|
||||
try {
|
||||
const messageListForGpt = historyMessageList
|
||||
.filter((it) => it.bizType !== 101 && it.isSelf)
|
||||
.slice(-recentMessageQuantityForLlm)
|
||||
await sendGptContent(pageMapByName.boss!, messageListForGpt)
|
||||
gtag('rnrr_llm_content_sent')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) {
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
gtag('rnrr_look_forward_reply_emotion_sent', {
|
||||
fallback: true
|
||||
})
|
||||
} else {
|
||||
gtag('rnrr_encounter_error', {
|
||||
error: err
|
||||
})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
gtag('rnrr_look_forward_reply_emotion_sent')
|
||||
}
|
||||
} else {
|
||||
cursorToContinueFind += 1
|
||||
}
|
||||
@@ -345,6 +400,17 @@ export async function runEntry() {
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
if (err instanceof Error && err.message === 'CANNOT_FIND_A_USABLE_MODEL') {
|
||||
gtag('cannot_find_a_usable_model')
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
message:
|
||||
'未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题',
|
||||
buttons: ['退出']
|
||||
})
|
||||
process.exit(0)
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
pageMapByName['boss'] = null
|
||||
await sleep(rerunInterval)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import buildInfo from '../../../common/build-info.json'
|
||||
import os from 'node:os'
|
||||
|
||||
type LowercaseLetter =
|
||||
| 'a'
|
||||
@@ -55,6 +56,7 @@ function getCommonParams() {
|
||||
return {
|
||||
app_version: buildInfo.version,
|
||||
app_build_hash: buildInfo.buildHash,
|
||||
os_info: `${os.type()} | ${os.release()} | ${os.arch()}`,
|
||||
t: Number(new Date())
|
||||
}
|
||||
}
|
||||
@@ -68,9 +70,14 @@ export default async function gtag<T extends string>(
|
||||
...getCommonParams(),
|
||||
...params
|
||||
}
|
||||
Object.keys(params).forEach((k) => {
|
||||
if ([null, undefined].includes(params[k])) {
|
||||
delete params[k]
|
||||
}
|
||||
})
|
||||
// ServiceWorker环境下直接调用上报函数
|
||||
const reporter = (await import('./Analytics')).default
|
||||
return reporter.fireEvent(name, params)
|
||||
return reporter.fireEvent(name.replace(/-/g, '_'), params)
|
||||
}
|
||||
|
||||
// Fire a page view event.
|
||||
|
||||
59
packages/ui/src/main/utils/initPublicIpc.ts
Normal file
59
packages/ui/src/main/utils/initPublicIpc.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { BrowserWindow, ipcMain, shell } from 'electron'
|
||||
import gtag from './gtag'
|
||||
import buildInfo from '../../common/build-info.json'
|
||||
import os from 'node:os'
|
||||
|
||||
export default function initPublicIpc() {
|
||||
ipcMain.on(
|
||||
'update-window-size',
|
||||
(
|
||||
ev,
|
||||
size: {
|
||||
width: number
|
||||
height: number
|
||||
animate?: boolean
|
||||
}
|
||||
) => {
|
||||
const win = BrowserWindow.fromWebContents(ev.sender)
|
||||
if (!win) {
|
||||
return
|
||||
}
|
||||
win.setMinimumSize(size.width, size.height)
|
||||
win.setSize(size.width, size.height, size.animate)
|
||||
}
|
||||
)
|
||||
ipcMain.on('open-external-link', (_, link) => {
|
||||
shell.openExternal(link, {
|
||||
activate: true
|
||||
})
|
||||
})
|
||||
ipcMain.on('gtag', (ev, { name, params } = {}) => {
|
||||
gtag(name, {
|
||||
...params,
|
||||
electron_log_source: 'renderer'
|
||||
})
|
||||
})
|
||||
ipcMain.on('send-feed-back-to-github-issue', (ev, payload) => {
|
||||
const getIssueUrlWithBody = (issueBody: string = '') => {
|
||||
const baseUrl = `https://github.com/geekgeekrun/geekgeekrun/issues/new`
|
||||
issueBody = issueBody || ''
|
||||
if (!issueBody || !issueBody.trim()) {
|
||||
return baseUrl
|
||||
}
|
||||
const urlObj = new URL(baseUrl)
|
||||
urlObj.searchParams.append('body', issueBody)
|
||||
|
||||
return urlObj.toString()
|
||||
}
|
||||
|
||||
shell.openExternal(
|
||||
getIssueUrlWithBody(`\n\n\n-----
|
||||
版本号:${buildInfo.version}(${buildInfo.buildVersion})
|
||||
提交:${buildInfo.buildHash.substring(0, 6)}
|
||||
操作系统信息: \`${os.type()}\` / \`${os.release()}\` / \`${os.arch()}\``),
|
||||
{
|
||||
activate: true
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -47,14 +47,5 @@ export const initIpc = () => {
|
||||
createFirstLaunchNoticeApproveFlag()
|
||||
firstLaunchNoticeWindow?.close()
|
||||
})
|
||||
ipcMain.on('update-window-size', (ev, size: {
|
||||
width: number, height: number, animate?: boolean
|
||||
}) => {
|
||||
const win = BrowserWindow.fromWebContents(ev.sender)
|
||||
if (!win) {
|
||||
return
|
||||
}
|
||||
win.setSize(size.width, size.height, size.animate)
|
||||
} )
|
||||
}
|
||||
initIpc()
|
||||
|
||||
44
packages/ui/src/main/window/llmConfigWindow.ts
Normal file
44
packages/ui/src/main/window/llmConfigWindow.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
export let llmConfigWindow: BrowserWindow | null = null
|
||||
export function createLlmConfigWindow(
|
||||
opt?: Electron.BrowserWindowConstructorOptions
|
||||
): BrowserWindow {
|
||||
// Create the browser window.
|
||||
if (llmConfigWindow) {
|
||||
llmConfigWindow!.show()
|
||||
}
|
||||
llmConfigWindow = new BrowserWindow({
|
||||
width: 576,
|
||||
height: 410,
|
||||
resizable: false,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
},
|
||||
...opt
|
||||
})
|
||||
|
||||
llmConfigWindow.on('ready-to-show', () => {
|
||||
llmConfigWindow!.show()
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
|
||||
llmConfigWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/llmConfig')
|
||||
} else {
|
||||
llmConfigWindow.loadURL(
|
||||
'file://' + path.join(__dirname, '../renderer/index.html') + '#/llmConfig'
|
||||
)
|
||||
}
|
||||
|
||||
llmConfigWindow!.once('closed', () => {
|
||||
llmConfigWindow = null
|
||||
})
|
||||
|
||||
return llmConfigWindow!
|
||||
}
|
||||
@@ -8,8 +8,9 @@ export let mainWindow: BrowserWindow | null = null
|
||||
export function createMainWindow(): BrowserWindow {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1024,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
minWidth: 1024,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux'
|
||||
@@ -23,8 +24,6 @@ export function createMainWindow(): BrowserWindow {
|
||||
}
|
||||
})
|
||||
|
||||
process.env.NODE_ENV === 'development' && openDevTools(mainWindow)
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
@@ -36,6 +35,12 @@ export function createMainWindow(): BrowserWindow {
|
||||
show: true
|
||||
})
|
||||
})
|
||||
mainWindow.on('ready-to-show', async () => {
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
setTimeout(() => {
|
||||
mainWindow && openDevTools(mainWindow)
|
||||
}, 500)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { BrowserWindow } from 'electron'
|
||||
import path from 'path'
|
||||
import { URL } from 'node:url'
|
||||
|
||||
export let readNoReplyReminderLlmMockWindow: BrowserWindow | null = null
|
||||
export function createReadNoReplyReminderLlmMockWindow(
|
||||
opt?: Electron.BrowserWindowConstructorOptions,
|
||||
{ autoReminderConfig } = {}
|
||||
): BrowserWindow {
|
||||
// Create the browser window.
|
||||
if (readNoReplyReminderLlmMockWindow) {
|
||||
readNoReplyReminderLlmMockWindow!.show()
|
||||
}
|
||||
readNoReplyReminderLlmMockWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 800,
|
||||
resizable: false,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
},
|
||||
...opt
|
||||
})
|
||||
|
||||
readNoReplyReminderLlmMockWindow.on('ready-to-show', () => {
|
||||
readNoReplyReminderLlmMockWindow!.show()
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
let urlObj: URL
|
||||
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
|
||||
urlObj = new URL(process.env['ELECTRON_RENDERER_URL'] + '#/readNoReplyReminderLlmMock')
|
||||
} else {
|
||||
urlObj = new URL(
|
||||
'file://' + path.join(__dirname, '../renderer/index.html') + '#/readNoReplyReminderLlmMock'
|
||||
)
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(autoReminderConfig || {})) {
|
||||
urlObj.searchParams.append(k, v)
|
||||
}
|
||||
readNoReplyReminderLlmMockWindow.loadURL(String(urlObj))
|
||||
readNoReplyReminderLlmMockWindow!.once('closed', () => {
|
||||
readNoReplyReminderLlmMockWindow = null
|
||||
})
|
||||
|
||||
return readNoReplyReminderLlmMockWindow!
|
||||
}
|
||||
44
packages/ui/src/main/window/resumeEditorWindow.ts
Normal file
44
packages/ui/src/main/window/resumeEditorWindow.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
export let resumeEditorWindow: BrowserWindow | null = null
|
||||
export function createResumeEditorWindow(
|
||||
opt?: Electron.BrowserWindowConstructorOptions
|
||||
): BrowserWindow {
|
||||
// Create the browser window.
|
||||
if (resumeEditorWindow) {
|
||||
resumeEditorWindow!.show()
|
||||
}
|
||||
resumeEditorWindow = new BrowserWindow({
|
||||
width: 960,
|
||||
height: 720,
|
||||
resizable: true,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
},
|
||||
...opt
|
||||
})
|
||||
|
||||
resumeEditorWindow.on('ready-to-show', () => {
|
||||
resumeEditorWindow!.show()
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
|
||||
resumeEditorWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/resumeEditor')
|
||||
} else {
|
||||
resumeEditorWindow.loadURL(
|
||||
'file://' + path.join(__dirname, '../renderer/index.html') + '#/resumeEditor'
|
||||
)
|
||||
}
|
||||
|
||||
resumeEditorWindow!.once('closed', () => {
|
||||
resumeEditorWindow = null
|
||||
})
|
||||
|
||||
return resumeEditorWindow!
|
||||
}
|
||||
@@ -9,4 +9,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import { onMounted } from 'vue'
|
||||
import { gtagRenderer } from './utils/gtag'
|
||||
|
||||
gtagRenderer('app_component_before_create')
|
||||
onMounted(() => {
|
||||
gtagRenderer('app_component_mounted')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
<template #header>
|
||||
<div class="diff-table-header">
|
||||
{{ transformUtcDateToLocalDate(item.value).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
<el-tooltip content="待对比条目少于2个" :disabled="tableProps.length > 1">
|
||||
<el-tooltip
|
||||
content="待对比条目少于2个"
|
||||
:disabled="tableProps.length > 1"
|
||||
@show="gtagRenderer('tooltip_show_about_lake_of_compare_item')"
|
||||
>
|
||||
<el-radio v-model="diffPivot" :label="item.value" :disabled="tableProps.length <= 1">作为diff基准</el-radio>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -44,6 +48,7 @@ import { JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/src/entity/JobInfoC
|
||||
import { ElTable, ElTableColumn, ElForm, ElFormItem, ElRow, ElCol, ElDivider } from 'element-plus'
|
||||
import TextDiff from '../../components/TextDiff.vue'
|
||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const props = defineProps({
|
||||
jobInfo: {
|
||||
|
||||
@@ -3,9 +3,20 @@
|
||||
<el-form-item label="公司">{{ jobInfo.companyName }}</el-form-item>
|
||||
<el-form-item label="职位名称">{{ jobInfo.jobName }}</el-form-item>
|
||||
<el-form-item label="职位分类">{{ jobInfo.positionName }}</el-form-item>
|
||||
<el-form-item label="开聊时间">{{
|
||||
transformUtcDateToLocalDate(jobInfo.date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}}</el-form-item>
|
||||
<el-form-item v-if="scene === 'startChatRecord'" label="开聊时间">
|
||||
{{
|
||||
jobInfo.date
|
||||
? transformUtcDateToLocalDate(jobInfo.date).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '无记录'
|
||||
}}
|
||||
</el-form-item>
|
||||
<el-form-item v-if="scene === 'markAsNotSuitRecord'" label="标记时间">
|
||||
{{
|
||||
jobInfo.date
|
||||
? transformUtcDateToLocalDate(jobInfo.date).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '无记录'
|
||||
}}
|
||||
</el-form-item>
|
||||
<el-form-item label="工作经验">{{ jobInfo.experienceName }}</el-form-item>
|
||||
<el-form-item label="薪资">{{
|
||||
`${jobInfo.salaryLow}-${jobInfo.salaryHigh}k` +
|
||||
@@ -14,19 +25,26 @@
|
||||
<el-form-item label="职位描述">
|
||||
<pre class="of-auto">{{ jobInfo.description }}</pre>
|
||||
</el-form-item>
|
||||
<el-form-item label="BOSS">{{ jobInfo.bossName }} - {{ jobInfo.bossTitle }}</el-form-item>
|
||||
<el-form-item label="BOSS"
|
||||
>{{ jobInfo.bossName
|
||||
}}<template v-if="jobInfo.bossTitle"> - {{ jobInfo.bossTitle }}</template></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
||||
import { type VMarkAsNotSuitLog } from '@geekgeekrun/sqlite-plugin/src/entity/VMarkAsNotSuitLog'
|
||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
jobInfo: {
|
||||
type: Object as PropType<VChatStartupLog>,
|
||||
type: Object as PropType<VChatStartupLog | VMarkAsNotSuitLog>,
|
||||
required: true
|
||||
},
|
||||
scene: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,13 +2,15 @@ import { createApp } from 'vue'
|
||||
import ElementPlus, { ElMessage } from 'element-plus'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'normalize.css'
|
||||
import './style/public.scss'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'virtual:uno.css'
|
||||
import 'animate.css'
|
||||
|
||||
createApp(App).use(router).use(ElementPlus).mount('#app')
|
||||
const pinia = createPinia()
|
||||
createApp(App).use(pinia).use(router).use(ElementPlus).mount('#app')
|
||||
electron.ipcRenderer.on('toast-message', (_, payload) => {
|
||||
ElMessage(payload)
|
||||
})
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -33,10 +34,12 @@ const checkDependenciesResult = ref({})
|
||||
const downloadProcessWaitee = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
gtagRenderer('bootstrap_mounted')
|
||||
checkDependenciesResult.value = await electron.ipcRenderer.invoke('check-dependencies')
|
||||
downloadProcessWaitee.value = Promise.withResolvers()
|
||||
|
||||
if (Object.values(checkDependenciesResult.value).includes(false)) {
|
||||
gtagRenderer('dependencies_need_download')
|
||||
router.replace('/downloadingDependencies')
|
||||
} else {
|
||||
downloadProcessWaitee.value!.resolve()
|
||||
@@ -45,10 +48,11 @@ onMounted(async () => {
|
||||
downloadProcessWaitee.value!.promise.then(async () => {
|
||||
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
|
||||
if (!isCookieFileValid) {
|
||||
gtagRenderer('found_cookie_invalid_when_bootstrap')
|
||||
router.replace('/cookieAssistant')
|
||||
} else {
|
||||
await sleep(1000)
|
||||
router.replace('/configuration')
|
||||
router.replace('/main-layout')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onUnmounted, PropType } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const props = defineProps({
|
||||
dependenciesStatus: {
|
||||
@@ -47,9 +48,11 @@ const processDownloadBrowser = async () => {
|
||||
const promiseList: Array<Promise<void>> = []
|
||||
const processTasks = async () => {
|
||||
if (!props.dependenciesStatus.puppeteerExecutableAvailable) {
|
||||
gtagRenderer('start_download_puppeteer')
|
||||
const p = processDownloadBrowser()
|
||||
promiseList.push(p)
|
||||
p.then(() => {
|
||||
gtagRenderer('puppeteer_download_success')
|
||||
props.dependenciesStatus.puppeteerExecutableAvailable = true
|
||||
})
|
||||
}
|
||||
@@ -64,6 +67,7 @@ const processTasks = async () => {
|
||||
})
|
||||
await p
|
||||
} catch {
|
||||
gtagRenderer('encounter_error_when_download_deps')
|
||||
await ElMessageBox.confirm('需要重试吗?', '核心组件下载失败', {
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
@@ -72,9 +76,11 @@ const processTasks = async () => {
|
||||
cancelButtonText: '退出程序'
|
||||
})
|
||||
.then(() => {
|
||||
gtagRenderer('start_retry_download_deps')
|
||||
processTasks()
|
||||
})
|
||||
.catch(() => {
|
||||
gtagRenderer('cancel_download_deps_and_exit')
|
||||
promiseList.length = 0
|
||||
electron.ipcRenderer.invoke('exit-app-immediately')
|
||||
})
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<div class="form-wrap">
|
||||
<el-form ref="formRef" :model="formContent" label-position="top" :rules="formRules">
|
||||
<el-form-item label="BOSS直聘 Cookie">
|
||||
<el-button size="small" type="primary" font-size-inherit @click="handleClickLaunchLogin"
|
||||
>编辑Cookie</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="钉钉机器人 AccessToken(用于记录开聊,请勿使用公司内部群)"
|
||||
prop="dingtalkRobotAccessToken"
|
||||
>
|
||||
<el-input v-model="formContent.dingtalkRobotAccessToken" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="期望职位白名单正则(按照职位名称+职位描述筛选职位,为空时将不按此条件筛选)"
|
||||
prop="expectJobRegExpStr"
|
||||
>
|
||||
<el-input v-model="formContent.expectJobRegExpStr" />
|
||||
</el-form-item>
|
||||
<el-form-item label="期望公司(以逗号分隔,为空时将不按此条件筛选)" prop="expectCompanies">
|
||||
<el-input
|
||||
v-model="formContent.expectCompanies"
|
||||
:autosize="{ minRows: 4 }"
|
||||
type="textarea"
|
||||
@blur="handleExpectCompaniesInputBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="推荐职位筛选器(当前求职期望找不到合适职位时,将尝试所有可能的筛选组合,查找新工作)"
|
||||
prop="filter"
|
||||
>
|
||||
<AnyCombineBossRecommendFilter v-model="formContent.anyCombineRecommendJobFilter" />
|
||||
<div>
|
||||
当前组合条件数:{{ currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString() }}
|
||||
<span
|
||||
v-if="
|
||||
currentAnyCombineRecommendJobFilterCombinationCount >= 10 &&
|
||||
currentAnyCombineRecommendJobFilterCombinationCount < 100
|
||||
"
|
||||
class="color-orange"
|
||||
>组合条件太多,建议少选择一些</span
|
||||
>
|
||||
<span
|
||||
v-if="currentAnyCombineRecommendJobFilterCombinationCount >= 100"
|
||||
class="color-orange"
|
||||
>你开心就好</span
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="标记不合适机制" class="color-orange">
|
||||
1. 如果查找到的职位活跃时间为“本月活跃”或更往前的时间,则这个职位将被标记为不合适<br />
|
||||
2. 如果查找到的职位,职位名称、职位类型、职位描述与期望职位白名单正则不匹配,则这个职位将被标记为不合适
|
||||
</el-form-item>
|
||||
<el-form-item class="last-form-item">
|
||||
<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 { useRouter } from 'vue-router'
|
||||
import AnyCombineBossRecommendFilter from '@renderer/features/AnyCombineBossRecommendFilter/index.vue'
|
||||
import { calculateTotalCombinations } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
|
||||
const router = useRouter()
|
||||
|
||||
const formContent = ref({
|
||||
dingtalkRobotAccessToken: '',
|
||||
expectCompanies: '',
|
||||
anyCombineRecommendJobFilter: {},
|
||||
expectJobRegExpStr: ''
|
||||
})
|
||||
|
||||
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: []
|
||||
}
|
||||
formContent.value.expectJobRegExpStr = res.config['boss.json']?.expectJobRegExpStr ?? ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
expectJobRegExpStr: {
|
||||
validator(_, value, cb) {
|
||||
if (!value) {
|
||||
cb()
|
||||
return
|
||||
}
|
||||
try {
|
||||
new RegExp(value, 'ig')
|
||||
cb()
|
||||
} catch (err) {
|
||||
cb(new Error(`正则无效:${err.message}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
const handleSubmit = async () => {
|
||||
formContent.value.expectJobRegExpStr = (formContent.value.expectJobRegExpStr || '').trim()
|
||||
await formRef.value!.validate()
|
||||
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' }
|
||||
})
|
||||
}
|
||||
const handleSave = async () => {
|
||||
await formRef.value!.validate()
|
||||
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
|
||||
ElMessage.success('Configuration saved.')
|
||||
}
|
||||
|
||||
const handleExpectCompaniesInputBlur = (event) => {
|
||||
event.target.value = (event.target?.value ?? '')
|
||||
.split(/,|,/)
|
||||
.map((it) => it.trim())
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
const handleClickLaunchLogin = () => {
|
||||
router.replace('/cookieAssistant')
|
||||
}
|
||||
</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>
|
||||
@@ -1,176 +0,0 @@
|
||||
<template>
|
||||
<div class="form-wrap">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:rules="formRules"
|
||||
:model="formContent.autoReminder"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item label="BOSS直聘 Cookie">
|
||||
<el-button size="small" type="primary" font-size-inherit @click="handleClickLaunchLogin"
|
||||
>编辑Cookie</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进话术" class="color-orange">
|
||||
当发现已读不回的Boss时,将向Boss发出“[盼回复]”表情
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
|
||||
<el-input-number
|
||||
v-model="formContent.autoReminder.throttleIntervalMinutes"
|
||||
class="w-150px"
|
||||
:min="3"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
@blur="handleThrottleIntervalMinutesBlur"
|
||||
/> 分钟内不多次跟进同一Boss
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进时限(天)" prop="rechatLimitDay">
|
||||
<div>
|
||||
<div><el-checkbox v-model="enableRechatLimit" /> 启用</div>
|
||||
<el-input-number
|
||||
v-model="formContent.autoReminder.rechatLimitDay"
|
||||
class="w-150px"
|
||||
:min="0"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
:disabled="!enableRechatLimit"
|
||||
/> 天<br />
|
||||
<div v-if="enableRechatLimit">
|
||||
不再跟进 (<span class="text-orange">{{ rechatLimitDateString }}</span>)之前列表中没有进展的聊天
|
||||
</div>
|
||||
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span>)</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item class="last-form-item">
|
||||
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { dayjs, ElForm } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
|
||||
const formContent = ref({
|
||||
autoReminder: {
|
||||
throttleIntervalMinutes: 10,
|
||||
rechatLimitDay: 21
|
||||
}
|
||||
})
|
||||
|
||||
const enableRechatLimit = computed({
|
||||
get() {
|
||||
return Boolean(formContent.value.autoReminder?.rechatLimitDay)
|
||||
},
|
||||
set(val) {
|
||||
if (!val) {
|
||||
formContent.value.autoReminder.rechatLimitDay = 0
|
||||
} else {
|
||||
formContent.value.autoReminder.rechatLimitDay = 21
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
|
||||
const conf = res.config['boss.json']?.autoReminder || {}
|
||||
conf.throttleIntervalMinutes = conf.throttleIntervalMinutes ?? 10
|
||||
conf.rechatLimitDay = conf.rechatLimitDay ?? 21
|
||||
|
||||
formContent.value.autoReminder = conf
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
throttleIntervalMinutes: {
|
||||
validator(_, value, cb) {
|
||||
if (/[^0-9.]/.test(String(value)) || isNaN(parseFloat(value)) || isNaN(Number(value))) {
|
||||
cb(new Error(`请输入数字!`))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
},
|
||||
rechatLimitDay: {
|
||||
validator(_, value, cb) {
|
||||
if (/[^0-9.]/.test(String(value)) || isNaN(parseFloat(value)) || isNaN(Number(value))) {
|
||||
cb(new Error(`请输入数字!`))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
watch(
|
||||
() => formContent.value.autoReminder,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
formRef.value?.validate?.()
|
||||
})
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value!.validate()
|
||||
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
|
||||
router.replace({
|
||||
path: '/geekAutoStartChatWithBoss/prepareRun',
|
||||
query: { flow: 'read-no-reply-reminder' }
|
||||
})
|
||||
}
|
||||
function handleThrottleIntervalMinutesBlur() {
|
||||
if (formContent.value.autoReminder.throttleIntervalMinutes < 3) {
|
||||
formContent.value.autoReminder.throttleIntervalMinutes = 3
|
||||
}
|
||||
formContent.value.autoReminder.throttleIntervalMinutes = Number(
|
||||
formContent.value.autoReminder.throttleIntervalMinutes
|
||||
)
|
||||
}
|
||||
|
||||
const handleClickLaunchLogin = () => {
|
||||
router.replace('/cookieAssistant')
|
||||
}
|
||||
|
||||
const currentStamp = ref(new Date())
|
||||
let timer = 0
|
||||
function updateCurrentStamp() {
|
||||
currentStamp.value = new Date()
|
||||
timer = window.setTimeout(updateCurrentStamp, 1000)
|
||||
}
|
||||
updateCurrentStamp()
|
||||
onUnmounted(() => {
|
||||
window.clearTimeout(timer)
|
||||
})
|
||||
|
||||
const rechatLimitDateString = computed(() => {
|
||||
return dayjs(
|
||||
+currentStamp.value - formContent.value.autoReminder.rechatLimitDay * 24 * 60 * 60 * 1000
|
||||
).format('YYYY-MM-DD HH:mm:ss')
|
||||
})
|
||||
</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: 40px;
|
||||
}
|
||||
.last-form-item {
|
||||
:deep(.el-form-item__content) {
|
||||
margin-top: 40px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h100vh">
|
||||
<div class="flex flex-col w160px pt30px pl30px aside-nav of-hidden">
|
||||
<div class="nav-list flex-1 of-auto">
|
||||
<RouterLink to="./GeekAutoStartChatWithBoss">Boss炸弹</RouterLink>
|
||||
<RouterLink to="./ReadNoReplyReminder">已读不回提醒器</RouterLink>
|
||||
<hr />
|
||||
<a href="javascript:void(0)" @click="handleLaunchBossSite">
|
||||
手动逛Boss<el-icon><TopRight /></el-icon>
|
||||
</a>
|
||||
<hr />
|
||||
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
|
||||
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
|
||||
<RouterLink to="./JobLibrary">职位库</RouterLink>
|
||||
<RouterLink to="./BossLibrary">Boss库</RouterLink>
|
||||
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
|
||||
</div>
|
||||
<div class="pt-16px pb-16px flex-0 font-size-12px">
|
||||
<div>当前版本: {{ buildInfo.version }}({{ buildInfo.buildVersion }})</div>
|
||||
<div class="feedback-area flex flex-items-center mt-8px">
|
||||
<el-button type="text" size="small" @click="handleGotoProjectPageClick"
|
||||
>项目首页</el-button
|
||||
>
|
||||
|
|
||||
<el-button type="text" size="small" @click="handleFeedbackClick">反馈问题</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RouterView v-slot="{ Component }" class="flex-1">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { TopRight } from '@element-plus/icons-vue'
|
||||
import useBuildInfo from '@renderer/hooks/useBuildInfo'
|
||||
import { debounce } from 'lodash-es'
|
||||
const router = useRouter()
|
||||
const unmountedCbs: Array<InstanceType<typeof Function>> = []
|
||||
onUnmounted(() => {
|
||||
while (unmountedCbs.length) {
|
||||
const fn = unmountedCbs.shift()!
|
||||
try {
|
||||
fn()
|
||||
} catch {}
|
||||
}
|
||||
})
|
||||
const goToCheckBossZhipinCookieFile = () => router.replace('/cookieAssistant')
|
||||
onMounted(() => {
|
||||
electron.ipcRenderer.on('check-boss-zhipin-cookie-file', goToCheckBossZhipinCookieFile)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
electron.ipcRenderer.removeListener(
|
||||
'check-boss-zhipin-cookie-file',
|
||||
goToCheckBossZhipinCookieFile
|
||||
)
|
||||
})
|
||||
;(async () => {
|
||||
const checkDependenciesResult = await electron.ipcRenderer.invoke('check-dependencies')
|
||||
if (Object.values(checkDependenciesResult).includes(false)) {
|
||||
router.replace('/')
|
||||
return
|
||||
}
|
||||
|
||||
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
|
||||
if (!isCookieFileValid) {
|
||||
router.replace('/cookieAssistant')
|
||||
return
|
||||
}
|
||||
})()
|
||||
|
||||
const { buildInfo } = useBuildInfo()
|
||||
const getIssueUrlWithBody = (issueBody: string = '') => {
|
||||
const baseUrl = `https://github.com/geekgeekrun/geekgeekrun/issues/new`
|
||||
issueBody = issueBody || ''
|
||||
if (!issueBody || !issueBody.trim()) {
|
||||
return baseUrl
|
||||
}
|
||||
const urlObj = new URL(baseUrl)
|
||||
urlObj.searchParams.append('body', issueBody)
|
||||
|
||||
return urlObj.toString()
|
||||
}
|
||||
const handleFeedbackClick = () => {
|
||||
electron.ipcRenderer.send(
|
||||
'open-external-link',
|
||||
getIssueUrlWithBody(`\n\n\n-----
|
||||
版本号:${buildInfo.value.version}(${buildInfo.value.buildVersion})
|
||||
提交:${buildInfo.value.buildHash.substring(0, 6)}`)
|
||||
)
|
||||
}
|
||||
const handleGotoProjectPageClick = () => {
|
||||
electron.ipcRenderer.send('open-external-link', 'https://github.com/geekgeekrun/geekgeekrun')
|
||||
}
|
||||
|
||||
const handleLaunchBossSite = debounce(
|
||||
async () => {
|
||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||
url: `https://www.zhipin.com/`
|
||||
})
|
||||
},
|
||||
1000,
|
||||
{ leading: true, trailing: false }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.aside-nav {
|
||||
background-image: linear-gradient(45deg, #eaf4f1, #dcf6f2);
|
||||
.nav-list {
|
||||
> a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.5em;
|
||||
box-sizing: border-box;
|
||||
padding-left: 2em;
|
||||
&.router-link-active {
|
||||
background-color: #fff;
|
||||
font-weight: 700;
|
||||
color: #2faa9e;
|
||||
border-radius: 9999px 0 0 9999px;
|
||||
}
|
||||
}
|
||||
> hr {
|
||||
border: 0 solid;
|
||||
height: 1px;
|
||||
background-color: #b3c8c3;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
width: 140px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.feedback-area {
|
||||
:deep(.el-button) {
|
||||
height: fit-content;
|
||||
padding: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="cookie-assistant-page">
|
||||
<div ml1em mt1em mb1em >Cookie 助手</div>
|
||||
<div ml1em mt1em mb1em font-size-16px>Boss 登录助手</div>
|
||||
<el-alert
|
||||
v-if="cookieInvalid"
|
||||
type="warning"
|
||||
@@ -11,11 +11,11 @@
|
||||
</el-alert>
|
||||
<div ml1em mt1em line-height-normal>
|
||||
如果您了解如何获取Cookie、了解有效的Cookie格式,可以直接在下方输入框中进行编辑。由于手动编辑较为麻烦,建议您打开已登录过Boss直聘的浏览器,使用<a
|
||||
color-blue
|
||||
decoration-none
|
||||
class="color-blue! decoration-none"
|
||||
href="javascript:void(0)"
|
||||
@click.prevent="handleEditThisCookieExtensionStoreLinkClick"
|
||||
>EditThisCookie 扩展程序</a
|
||||
>
|
||||
EditThisCookie 扩展程序 </a
|
||||
>复制Cookie,然后粘贴在下方输入框中。文本格式为被序列化为JSON的数组,不含两侧引号。
|
||||
</div>
|
||||
<br />
|
||||
@@ -85,7 +85,12 @@
|
||||
size="small"
|
||||
type="primary"
|
||||
font-size-inherit
|
||||
@click="fillCollectedCookie"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('replace_inputted_cookie_by_collected')
|
||||
fillCollectedCookie()
|
||||
}
|
||||
"
|
||||
>使用获取到的Cookie</el-button
|
||||
></template
|
||||
></el-alert
|
||||
@@ -103,7 +108,8 @@
|
||||
import { ElForm, ElMessage } from 'element-plus'
|
||||
import { ref, onUnmounted, onMounted } from 'vue'
|
||||
import { checkCookieListFormat } from '../../../../common/utils/cookie'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter } from 'vue-router'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const router = useRouter()
|
||||
const cookieInvalid = ref(false)
|
||||
@@ -138,11 +144,13 @@ const formRules = {
|
||||
`JSON格式无效 - 存在语法错误: ${err.message};建议使用EditThisCookie扩展程序进行复制。`
|
||||
)
|
||||
)
|
||||
gtagRenderer('wrong_cookie_format_json_syntax_error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!checkCookieListFormat(JSON.parse(formContent.value.collectedCookies))) {
|
||||
cb(new Error(`Cookie格式无效 - 部分字段缺失;建议使用EditThisCookie扩展程序进行复制。`))
|
||||
gtagRenderer('wrong_cookie_format_field_loss')
|
||||
return
|
||||
}
|
||||
cb()
|
||||
@@ -158,6 +166,9 @@ const handleCookieCollected = (_, payload) => {
|
||||
collectedCookie.value = payload.cookies
|
||||
if (!hasUserMutateInput.value) {
|
||||
fillCollectedCookie()
|
||||
gtagRenderer('cookie_collected_and_auto_filled')
|
||||
} else {
|
||||
gtagRenderer('cookie_collected_after_changed_input')
|
||||
}
|
||||
}
|
||||
const fillCollectedCookie = () => {
|
||||
@@ -169,28 +180,33 @@ const fillCollectedCookie = () => {
|
||||
}
|
||||
|
||||
const handleClickLaunchLogin = () => {
|
||||
gtagRenderer('launch_login_button_clicked')
|
||||
electron.ipcRenderer.send('launch-bosszhipin-login-page-with-preload-extension')
|
||||
loginCookieWaitingStatus.value = LOGIN_COOKIE_WAITING_STATUS.WAITING_FOR_LOGIN
|
||||
}
|
||||
|
||||
const handleEditThisCookieExtensionStoreLinkClick = () => {
|
||||
gtagRenderer('etc_extension_link_clicked')
|
||||
electron.ipcRenderer.send(
|
||||
'open-external-link',
|
||||
'https://chromewebstore.google.com/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg'
|
||||
'https://chromewebstore.google.com/detail/editthiscookie-v3/ojfebgpkimhlhcblbalbfjblapadhbol'
|
||||
)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
router.replace('/configuration')
|
||||
gtagRenderer('cancel_clicked')
|
||||
router.replace('/main-layout')
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
gtagRenderer('save_clicked')
|
||||
await formRef.value!.validate()
|
||||
await electron.ipcRenderer.invoke('write-storage-file', {
|
||||
fileName: 'boss-cookies.json',
|
||||
data: formContent.value.collectedCookies
|
||||
})
|
||||
ElMessage.success('Boss直聘 Cookie 保存成功')
|
||||
router.replace('/configuration')
|
||||
gtagRenderer('save_cookie_done')
|
||||
router.replace('/main-layout')
|
||||
}
|
||||
|
||||
const handleBossZhipinLoginPageClosed = () => {
|
||||
@@ -199,6 +215,9 @@ const handleBossZhipinLoginPageClosed = () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gtagRenderer('cookie_assistant_mounted')
|
||||
})
|
||||
onMounted(async () => {
|
||||
electron.ipcRenderer.once('BOSS_ZHIPIN_COOKIE_COLLECTED', handleCookieCollected)
|
||||
electron.ipcRenderer.on('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED', handleBossZhipinLoginPageClosed)
|
||||
@@ -226,6 +245,7 @@ onUnmounted(() => {
|
||||
.cookie-assistant-page {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus'
|
||||
import { ref, onMounted, onBeforeMount } from 'vue';
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const electron = window.electron
|
||||
|
||||
const readmeItemCheckStatusList = ref<number[]>([])
|
||||
@@ -97,8 +99,10 @@ const handleCancel = () => {
|
||||
|
||||
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
|
||||
const handleSubmit = () => {
|
||||
gtagRenderer('submit_clicked')
|
||||
const COUNT = 9
|
||||
if (readmeItemCheckStatusList.value.length !== COUNT) {
|
||||
gtagRenderer('agreement_not_finish_read_tip_displayed')
|
||||
ElMessage.warning(
|
||||
`您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`
|
||||
)
|
||||
@@ -111,6 +115,7 @@ const handleSubmit = () => {
|
||||
return
|
||||
}
|
||||
electron.ipcRenderer.invoke('first-launch-notice-approve')
|
||||
gtagRenderer('submit_done')
|
||||
}
|
||||
const handleReadmeItemCheckStatusListChange = (value: number[]) => {
|
||||
value.forEach((it) => {
|
||||
|
||||
@@ -18,22 +18,26 @@ import { ref, onUnmounted, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FlyingCompanyLogoList from '../../features/FlyingCompanyLogoList/index.vue'
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const { ipcRenderer } = electron
|
||||
const router = useRouter()
|
||||
|
||||
const handleStopButtonClick = async () => {
|
||||
gtagRenderer('gascwb_stop_button_clicked')
|
||||
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
|
||||
}
|
||||
|
||||
const isStopping = ref(false)
|
||||
const handleStopping = () => {
|
||||
gtagRenderer('gascwb_become_stopping')
|
||||
isStopping.value = true
|
||||
}
|
||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
||||
|
||||
const handleStopped = () => {
|
||||
router.replace('/configuration/GeekAutoStartChatWithBoss')
|
||||
gtagRenderer('gascwb_become_stopped')
|
||||
router.replace('/main-layout/GeekAutoStartChatWithBoss')
|
||||
}
|
||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopped', handleStopped)
|
||||
|
||||
@@ -47,12 +51,14 @@ onMounted(async () => {
|
||||
await electron.ipcRenderer.invoke('run-geek-auto-start-chat-with-boss')
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
||||
gtagRenderer('gascwb_cannot_run_for_corrupt')
|
||||
ElMessage.error({
|
||||
message: `核心组件损坏,正在尝试修复`
|
||||
})
|
||||
router.replace('/')
|
||||
}
|
||||
console.error(err)
|
||||
gtagRenderer('gascwb_cannot_run_for_unknown_error', { err })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -16,22 +16,26 @@ import { ref, onUnmounted, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FlyingCompanyLogoList from '../../features/FlyingCompanyLogoList/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const { ipcRenderer } = electron
|
||||
const router = useRouter()
|
||||
|
||||
const handleStopButtonClick = async () => {
|
||||
gtagRenderer('rnrr_stop_button_clicked')
|
||||
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
|
||||
}
|
||||
|
||||
const isStopping = ref(false)
|
||||
const handleStopping = () => {
|
||||
gtagRenderer('rnrr_become_stopping')
|
||||
isStopping.value = true
|
||||
}
|
||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
||||
|
||||
const handleStopped = () => {
|
||||
router.replace('/configuration/ReadNoReplyReminder')
|
||||
gtagRenderer('rnrr_become_stopped')
|
||||
router.replace('/main-layout/ReadNoReplyReminder')
|
||||
}
|
||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopped', handleStopped)
|
||||
|
||||
@@ -45,12 +49,14 @@ onMounted(async () => {
|
||||
await electron.ipcRenderer.invoke('run-read-no-reply-auto-reminder')
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
||||
gtagRenderer('rnrr_cannot_run_for_corrupt')
|
||||
ElMessage.error({
|
||||
message: `核心组件损坏,正在尝试修复`
|
||||
})
|
||||
router.replace('/')
|
||||
}
|
||||
console.error(err)
|
||||
gtagRenderer('rnrr_cannot_run_for_unknown_error', { err })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
565
packages/ui/src/renderer/src/page/LlmConfig/index.vue
Normal file
565
packages/ui/src/renderer/src/page/LlmConfig/index.vue
Normal file
@@ -0,0 +1,565 @@
|
||||
<template>
|
||||
<div class="llm-config-page">
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
<div class="mt1em mb1em">
|
||||
<div class="flex flex-items-center flex-justify-between">
|
||||
<div>大语言模型设置</div>
|
||||
<el-dropdown
|
||||
@command="
|
||||
(item) => {
|
||||
gtagRenderer('provider_url_for_secret_clicked', {
|
||||
name: item.name
|
||||
})
|
||||
openExternalLink(item.url)
|
||||
}
|
||||
"
|
||||
>
|
||||
<el-button size="small"
|
||||
>获取 API Secret <el-icon class="el-icon--right"><arrow-down /></el-icon
|
||||
></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="item in providerList" :key="item.name" :command="item">{{
|
||||
item.name
|
||||
}}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert type="info" :closable="false" mb20px line-height-1.25em>
|
||||
<ul pl16px m0>
|
||||
<li>
|
||||
请确保当前服务商提供的模型支持<a
|
||||
:style="{
|
||||
color: 'var(--el-color-primary)'
|
||||
}"
|
||||
href="javascript:void(0)"
|
||||
@click.prevent="
|
||||
() => {
|
||||
gtagRenderer('chat_completion_intro_doc_link_clicked')
|
||||
openExternalLink(
|
||||
'https://api-docs.deepseek.com/zh-cn/api/create-chat-completion'
|
||||
)
|
||||
}
|
||||
"
|
||||
>对话补全</a
|
||||
>且兼容
|
||||
<a
|
||||
:style="{
|
||||
color: 'var(--el-color-primary)'
|
||||
}"
|
||||
href="javascript:void(0)"
|
||||
@click.prevent="
|
||||
() => {
|
||||
gtagRenderer('openai_sdk_intro_doc_link_clicked')
|
||||
openExternalLink('https://www.npmjs.com/package/openai')
|
||||
}
|
||||
"
|
||||
>OpenAI SDK</a
|
||||
>
|
||||
</li>
|
||||
<li><b class="color-red">暂不支持推理模型</b>(例如 DeepSeek-R1)</li>
|
||||
<li>
|
||||
请自行确保您所接入的服务商能够保护您的隐私。<b class="color-red"
|
||||
>此处所列举“服务商-模型”由第三方提供,仅供配置参考,本程序不能保证它们能够合法使用您的数据,不表示本程序认可相关模型</b
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</el-alert>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formContentForElForm"
|
||||
:rules="formRulesForElForm"
|
||||
label-position="top"
|
||||
class="llm-config-form"
|
||||
:validate-on-rule-change="false"
|
||||
>
|
||||
<div v-for="(conf, index) in formContent" :key="conf.id" class="flex gap12px">
|
||||
<div
|
||||
v-if="formContent.length > 1"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
height: 'fit-content',
|
||||
marginTop: '10px'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveConfigUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= formContent.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveConfigDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="1 >= formContent.length"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeConfig(index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<el-form-item :prop="`${index}_providerCompleteApiUrl`">
|
||||
<div
|
||||
class="el-form-item__label flex-items-center flex-justify-between w-full pr0px"
|
||||
:style="{ display: 'flex' }"
|
||||
>
|
||||
<div>服务提供商 Base URL</div>
|
||||
<el-dropdown @command="(item) => handlePresetClick(item, index)">
|
||||
<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 llmPresetList"
|
||||
:key="item.name"
|
||||
:command="item"
|
||||
>{{ item.name }}</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<el-input
|
||||
ref="firstInputRefList"
|
||||
v-model="conf.providerCompleteApiUrl"
|
||||
:autosize="{
|
||||
minRows: 10,
|
||||
maxRows: 10
|
||||
}"
|
||||
font-size-12px
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="model" label="要使用的模型(model参数)">
|
||||
<el-input
|
||||
v-model="conf.model"
|
||||
:autosize="{
|
||||
minRows: 10,
|
||||
maxRows: 10
|
||||
}"
|
||||
font-size-12px
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="providerApiSecret" label="从服务提供商处获取的 API Secret">
|
||||
<el-input
|
||||
v-model="conf.providerApiSecret"
|
||||
:autosize="{
|
||||
minRows: 10,
|
||||
maxRows: 10
|
||||
}"
|
||||
font-size-12px
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<div class="serve-weight-config">
|
||||
<div class="flex">
|
||||
<el-form-item prop="enabled">
|
||||
<div class="el-form-item__label">启用</div>
|
||||
<el-checkbox
|
||||
v-if="formContent.length > 1"
|
||||
v-model="conf.enabled"
|
||||
font-size-12px
|
||||
@click="
|
||||
nextTick(() =>
|
||||
gtagRenderer('model_enable_status_changed', { enabled: conf.enabled })
|
||||
)
|
||||
"
|
||||
></el-checkbox>
|
||||
<el-checkbox v-else :model-value="true" font-size-12px disabled />
|
||||
</el-form-item>
|
||||
<el-form-item prop="serveWeight" class="ml40px">
|
||||
<div class="el-form-item__label">权重</div>
|
||||
<el-input-number
|
||||
v-if="formContent.length > 1"
|
||||
v-model="conf.serveWeight"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
step-strictly
|
||||
:precision="0"
|
||||
font-size-12px
|
||||
placeholder="1 ~ 100"
|
||||
@change="
|
||||
(new_val, old_val) => {
|
||||
gtagRenderer('serve_weight_changed', { new_val, old_val })
|
||||
}
|
||||
"
|
||||
></el-input-number>
|
||||
<el-input-number
|
||||
v-else
|
||||
font-size-12px
|
||||
:model-value="SINGLE_ITEM_DEFAULT_SERVE_WEIGHT"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<!-- <el-form-item class="ml20px">
|
||||
<el-button type="text">测试设置</el-button>
|
||||
</el-form-item> -->
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="index !== formContent.length - 1"
|
||||
class="mt6px mb20px h1px"
|
||||
style="background-color: #dcdcdc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</main>
|
||||
</div>
|
||||
<footer pt10px pb10px flex flex-justify-center>
|
||||
<div w480px flex flex-justify-between>
|
||||
<div>
|
||||
<el-button font-size-12px type="text" @click="addConfig"
|
||||
>添加备用模型<span v-if="formContent.length <= 1">,以生成更随机的内容</span></el-button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
ElIcon,
|
||||
ElButton,
|
||||
ElInput,
|
||||
ElMessage
|
||||
} from 'element-plus'
|
||||
import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue'
|
||||
import { ref, onMounted, watch, nextTick, computed } from 'vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../../common/constant'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
interface LlmConfigItem {
|
||||
id: string
|
||||
providerCompleteApiUrl: string
|
||||
providerApiSecret: string
|
||||
model: string
|
||||
serveWeight: number
|
||||
enabled: true
|
||||
}
|
||||
|
||||
function getNewConfigItem(): LlmConfigItem {
|
||||
return {
|
||||
id: uuid(),
|
||||
providerCompleteApiUrl: '',
|
||||
providerApiSecret: '',
|
||||
model: '',
|
||||
serveWeight: 10,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
const formContent = ref<LlmConfigItem[]>([getNewConfigItem()])
|
||||
|
||||
const formContentForElForm = computed(() => {
|
||||
const valueMap = {}
|
||||
formContent.value.forEach((configItem, i) => {
|
||||
valueMap[`${i}_providerCompleteApiUrl`] = configItem.providerCompleteApiUrl
|
||||
})
|
||||
return valueMap
|
||||
})
|
||||
const formRulesForElForm = computed(() => {
|
||||
const valueMap = {}
|
||||
formContent.value.forEach((_, i) => {
|
||||
valueMap[`${i}_providerCompleteApiUrl`] = [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入服务提供商 Base URL'
|
||||
},
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator(_, value, cb) {
|
||||
try {
|
||||
new URL(value?.trim())
|
||||
} catch (err) {
|
||||
cb(`URL 格式无效,请重新输入`)
|
||||
}
|
||||
if (/^http(s)?:\/\//.test(value)) {
|
||||
cb()
|
||||
return
|
||||
}
|
||||
cb(`服务提供商 Base URL 无效,请重新输入`)
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
return valueMap
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
gtagRenderer('cancel_clicked')
|
||||
electron.ipcRenderer.send('close-llm-config')
|
||||
gtagRenderer('cancel_done')
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
gtagRenderer('submit_clicked', { llm_config_length: formContent.value.length })
|
||||
await formRef.value?.validate()
|
||||
if (!formContent.value.length) {
|
||||
gtagRenderer('empty_model_list')
|
||||
ElMessage.warning({
|
||||
message: '可选模型列表为空,请出现填写'
|
||||
})
|
||||
formContent.value = [getNewConfigItem()]
|
||||
return
|
||||
} else if (formContent.value.length > 1) {
|
||||
const firstEnabledModel = formContent.value.find(it => it.enabled)
|
||||
if (!firstEnabledModel) {
|
||||
gtagRenderer('no_enabled_model_find_in_model_list')
|
||||
ElMessage.warning({
|
||||
dangerouslyUseHTMLString: true,
|
||||
grouping: true,
|
||||
message: '<div style="white-space: nowrap">所有模型均被禁用;请至少启用一个模型</div>'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
electron.ipcRenderer.invoke('save-llm-config', JSON.parse(JSON.stringify(formContent.value)))
|
||||
gtagRenderer('submit_done')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const savedFileContent = (await electron.ipcRenderer.invoke('fetch-config-file-content'))
|
||||
?.config?.['llm.json']
|
||||
if (!savedFileContent?.length) {
|
||||
return
|
||||
}
|
||||
const keyOfItem = Object.keys(getNewConfigItem())
|
||||
formContent.value = savedFileContent.map((it) => {
|
||||
const conf: any = {}
|
||||
for (const k of keyOfItem) {
|
||||
conf[k] = it[k]
|
||||
}
|
||||
if (!it.id) {
|
||||
conf.id = uuid()
|
||||
}
|
||||
return conf
|
||||
})
|
||||
})
|
||||
|
||||
const llmPresetList: {
|
||||
name: string
|
||||
config: Omit<LlmConfigItem, 'id'>
|
||||
}[] = [
|
||||
{
|
||||
name: '由 DeepSeek 提供的 DeepSeek-V3 模型',
|
||||
config: {
|
||||
model: 'deepseek-chat',
|
||||
providerApiSecret: '',
|
||||
providerCompleteApiUrl: 'https://api.deepseek.com/v1',
|
||||
serveWeight: 100,
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '由 火山引擎 提供的 DeepSeek-V3 模型',
|
||||
config: {
|
||||
model: 'deepseek-v3-250324',
|
||||
providerApiSecret: '',
|
||||
providerCompleteApiUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
serveWeight: 100,
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '由 阿里云百炼 提供的 DeepSeek-V3 模型',
|
||||
config: {
|
||||
model: 'deepseek-v3',
|
||||
providerApiSecret: '',
|
||||
providerCompleteApiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
serveWeight: 100,
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
// TODO:
|
||||
// {
|
||||
// name: '通过 Ollama 部署的 DeepSeek-R1(14B)模型',
|
||||
// config: {
|
||||
// model: 'deepseek-r1:14b',
|
||||
// providerApiSecret: 'ollama',
|
||||
// providerCompleteApiUrl: 'http://127.0.0.1:11434/v1',
|
||||
// serveWeight: 10,
|
||||
// enabled: true
|
||||
// }
|
||||
// },
|
||||
{
|
||||
name: '由 free.v36.cm 提供的 GPT-4o mini 模型',
|
||||
config: {
|
||||
model: 'gpt-4o-mini',
|
||||
providerApiSecret: 'sk-P3kvkV6UZ9WMy6AH792480Fc5e1c4dAb8aE17b20FcAc4eC3',
|
||||
providerCompleteApiUrl: 'https://free.v36.cm/v1',
|
||||
serveWeight: 20,
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '通过 Ollama 部署的 Qwen2.5(7B)模型',
|
||||
config: {
|
||||
model: 'qwen2.5:7b',
|
||||
providerApiSecret: 'ollama',
|
||||
providerCompleteApiUrl: 'http://127.0.0.1:11434/v1',
|
||||
serveWeight: 10,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const providerList: Array<{ name: string; url: string }> = [
|
||||
{
|
||||
name: 'DeepSeek',
|
||||
url: 'https://platform.deepseek.com/'
|
||||
},
|
||||
{
|
||||
name: '火山引擎 - 火山方舟',
|
||||
url: 'https://console.volcengine.com/ark'
|
||||
},
|
||||
{
|
||||
name: '阿里云百炼',
|
||||
url: 'https://bailian.console.aliyun.com/?tab=model#/api-key'
|
||||
},
|
||||
{
|
||||
name: 'OpenAI (国内可能不可用)',
|
||||
url: 'https://platform.openai.com/api-keys'
|
||||
},
|
||||
{
|
||||
name: 'FREE-CHATGPT-API (免费)',
|
||||
url: 'https://github.com/popjane/free_chatgpt_api'
|
||||
}
|
||||
]
|
||||
|
||||
function handlePresetClick(selected: (typeof llmPresetList)[number], index) {
|
||||
gtagRenderer('model_preset_clicked', {
|
||||
name: selected.name
|
||||
})
|
||||
for (const k of Object.keys(formContent.value[index])) {
|
||||
formContent.value[index][k] = selected.config[k]
|
||||
}
|
||||
if (!formContent.value[index].id) {
|
||||
formContent.value[index].id = uuid()
|
||||
}
|
||||
}
|
||||
|
||||
const firstInputRefList = ref<InstanceType<typeof ElInput>[]>([])
|
||||
function addConfig() {
|
||||
gtagRenderer('new_config_item_added', { config_list_length_before_add: formContent.value.length })
|
||||
formContent.value.push(getNewConfigItem())
|
||||
nextTick(() => {
|
||||
firstInputRefList.value[firstInputRefList.value.length - 1]?.focus()
|
||||
})
|
||||
}
|
||||
function moveConfigUp(index) {
|
||||
;[formContent.value[index], formContent.value[index - 1]] = [
|
||||
formContent.value[index - 1],
|
||||
formContent.value[index]
|
||||
]
|
||||
gtagRenderer('config_item_moved_up')
|
||||
}
|
||||
|
||||
function moveConfigDown(index) {
|
||||
;[formContent.value[index], formContent.value[index + 1]] = [
|
||||
formContent.value[index + 1],
|
||||
formContent.value[index]
|
||||
]
|
||||
gtagRenderer('config_item_moved_down')
|
||||
}
|
||||
|
||||
function removeConfig(index) {
|
||||
formContent.value.splice(index, 1)
|
||||
gtagRenderer('config_item_removed')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formContent.value.length,
|
||||
async (nVal) => {
|
||||
await sleep(100)
|
||||
if (nVal <= 1) {
|
||||
electron.ipcRenderer.send('update-window-size', {
|
||||
width: window.innerWidth,
|
||||
height: 576
|
||||
})
|
||||
} else {
|
||||
electron.ipcRenderer.send('update-window-size', {
|
||||
width: window.innerWidth,
|
||||
height: 730
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
const openExternalLink = (url) => {
|
||||
electron.ipcRenderer.send('open-external-link', url)
|
||||
}
|
||||
|
||||
// function handleTestAvailability() {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.llm-config-page {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
.main-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
main {
|
||||
margin: 0 auto;
|
||||
max-width: 480px;
|
||||
}
|
||||
}
|
||||
footer {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.llm-config-form.el-form {
|
||||
.el-form-item__error--inline {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.serve-weight-config {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.el-form-item__label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.el-form-item__content {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,17 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getBossLibrary">刷新</el-button>
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('boss_library_refresh_clicked')
|
||||
getBossLibrary()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.pageNo"
|
||||
@@ -42,6 +52,7 @@ import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ElTable, ElTableColumn, ElButton, ElPagination } from 'element-plus'
|
||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VChatStartupLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -54,6 +65,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getBossLibrary() {
|
||||
try {
|
||||
gtagRenderer('boss_library_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-boss-library', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -65,7 +80,16 @@ async function getBossLibrary() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('boss_library_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('boss_library_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
@@ -23,7 +23,15 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getCompanyLibrary"
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('company_library_refresh_clicked')
|
||||
getCompanyLibrary()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -49,6 +57,7 @@ import { ElTable, ElTableColumn, ElButton, ElPagination } from 'element-plus'
|
||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import { formatCompanyScale } from '@geekgeekrun/sqlite-plugin/src/utils/parser'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VChatStartupLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -64,6 +73,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getCompanyLibrary() {
|
||||
try {
|
||||
gtagRenderer('company_library_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-company-library', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -75,7 +88,16 @@ async function getCompanyLibrary() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('company_library_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('company_library_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
@@ -0,0 +1,703 @@
|
||||
<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>
|
||||
</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>
|
||||
<div>
|
||||
<el-form-item mb0>
|
||||
查看职位详情后,发现当前职位名称/类型/描述不符合投递条件时,将要执行的操作
|
||||
</el-form-item>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1.25fr 0.75fr',
|
||||
gap: '10px 0',
|
||||
width: '100%',
|
||||
alignItems: 'end'
|
||||
}"
|
||||
>
|
||||
<el-form-item mb0>
|
||||
<el-select
|
||||
v-model="formContent.jobNotMatchStrategy"
|
||||
@change="(value) => gtagRenderer('job_not_match_strategy_changed', { value })"
|
||||
>
|
||||
<el-option
|
||||
v-for="op in strategyOptionWhenCurrentJobNotMatch"
|
||||
:key="op.value"
|
||||
:label="op.name"
|
||||
:value="op.value"
|
||||
>{{ op.name }}</el-option
|
||||
>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div />
|
||||
</div>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
placement="bottom-start"
|
||||
@show="gtagRenderer('tooltip_show_about_wrongly_mark_not_suit')"
|
||||
>
|
||||
<template #content>
|
||||
<ul m0 line-height-1.5em w-400px pl2em>
|
||||
<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>
|
||||
</ul>
|
||||
</template>
|
||||
<el-button type="text" font-size-12px
|
||||
><span><QuestionFilled w-1em h-1em mr2px /></span>职位被错误标记时如何处理?</el-button
|
||||
>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-form-item mb0> 查看职位详情后,发现当前职位不活跃时,将要执行的操作 </el-form-item>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1.25fr 0.75fr',
|
||||
gap: '10px 0',
|
||||
width: '100%',
|
||||
alignItems: 'end'
|
||||
}"
|
||||
>
|
||||
<el-form-item>
|
||||
<div font-size-12px>认为职位不活跃的时间范围:</div>
|
||||
<el-slider
|
||||
v-model="formContent.markAsNotActiveSelectedTimeRange"
|
||||
:marks="noActiveDefinitionMarks"
|
||||
:max="10"
|
||||
:step="1"
|
||||
pl50px
|
||||
pr0
|
||||
class="no-active-definition-text-slider"
|
||||
:format-tooltip="
|
||||
(v) =>
|
||||
typeof noActiveDefinitionMarks[v] === 'string'
|
||||
? noActiveDefinitionMarks[v]
|
||||
: noActiveDefinitionMarks[v]?.label
|
||||
"
|
||||
@change="(value) => gtagRenderer('job_not_active_time_range_changed', { value })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div />
|
||||
<el-form-item v-if="formContent.markAsNotActiveSelectedTimeRange > 0" mb0>
|
||||
<div font-size-12px>发现当前职位不活跃时:</div>
|
||||
<el-select
|
||||
v-model="formContent.jobNotActiveStrategy"
|
||||
@change="(value) => gtagRenderer('job_not_active_strategy_changed', { value })"
|
||||
>
|
||||
<el-option
|
||||
v-for="op in strategyOptionWhenCurrentJobNotMatch"
|
||||
:key="op.value"
|
||||
:label="op.name"
|
||||
:value="op.value"
|
||||
>{{ op.name }}</el-option
|
||||
>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
placement="bottom-start"
|
||||
@show="gtagRenderer('tooltip_show_about_wrongly_mark_not_suit')"
|
||||
>
|
||||
<template #content>
|
||||
<ul m0 line-height-1.5em w-400px pl2em>
|
||||
<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>
|
||||
</ul>
|
||||
</template>
|
||||
<el-button type="text" font-size-12px
|
||||
><span><QuestionFilled w-1em h-1em mr2px /></span
|
||||
>职位被错误标记时如何处理?</el-button
|
||||
>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item
|
||||
label="职位备选筛选条件(当前求职期望无合适职位时,自动更改Boss筛选条件,查找新工作)"
|
||||
prop="filter"
|
||||
mt20px
|
||||
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 mb18px">
|
||||
<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, onBeforeUnmount, ref, watch } 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 { activeDescList } from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs'
|
||||
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'
|
||||
import { MarkAsNotSuitOp } from '@geekgeekrun/sqlite-plugin/src/enums'
|
||||
import { debounce } from 'lodash-es'
|
||||
import mittBus from '../../utils/mitt'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const formContent = ref({
|
||||
dingtalkRobotAccessToken: '',
|
||||
expectCompanies: '',
|
||||
anyCombineRecommendJobFilter: {},
|
||||
expectJobNameRegExpStr: '',
|
||||
expectJobTypeRegExpStr: '',
|
||||
expectJobDescRegExpStr: '',
|
||||
jobNotMatchStrategy: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
jobNotActiveStrategy: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
|
||||
markAsNotActiveSelectedTimeRange: 7
|
||||
})
|
||||
|
||||
const currentAnyCombineRecommendJobFilterCombinationCount = computed(() => {
|
||||
return calculateTotalCombinations(formContent.value.anyCombineRecommendJobFilter)
|
||||
})
|
||||
|
||||
const unwatchAnyCombineRecommendJobFilter = ref<null | (() => void)>(null)
|
||||
onBeforeUnmount(() => {
|
||||
unwatchAnyCombineRecommendJobFilter.value?.()
|
||||
})
|
||||
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: []
|
||||
}
|
||||
unwatchAnyCombineRecommendJobFilter.value = watch(
|
||||
() => formContent.value?.anyCombineRecommendJobFilter,
|
||||
debounce(() => {
|
||||
gtagRenderer('any_combine_filter_changed')
|
||||
}, 2000),
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
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()
|
||||
|
||||
formContent.value.jobNotMatchStrategy = strategyOptionWhenCurrentJobNotMatch
|
||||
.map((it) => it.value)
|
||||
.includes(res.config['boss.json'].jobNotMatchStrategy)
|
||||
? res.config['boss.json'].jobNotMatchStrategy
|
||||
: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
formContent.value.markAsNotActiveSelectedTimeRange = isNaN(
|
||||
parseInt(String(res.config['boss.json'].markAsNotActiveSelectedTimeRange))
|
||||
)
|
||||
? 7
|
||||
: parseInt(String(res.config['boss.json'].markAsNotActiveSelectedTimeRange))
|
||||
|
||||
formContent.value.jobNotActiveStrategy = strategyOptionWhenCurrentJobNotMatch
|
||||
.map((it) => it.value)
|
||||
.includes(res.config['boss.json'].jobNotActiveStrategy)
|
||||
? res.config['boss.json'].jobNotActiveStrategy
|
||||
: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
})
|
||||
|
||||
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))
|
||||
mittBus.emit('auto-start-chat-with-boss-config-saved')
|
||||
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))
|
||||
mittBus.emit('auto-start-chat-with-boss-config-saved')
|
||||
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,车欢欢,五八,红布林,致美,快狗,天鹅到家,转转,美餐,知乎,智者四海,易点云,搜狐,用友,畅捷通,猿辅导,小猿,猿力,好未来,学而思,希望学,新东方,东方甄选,东方优选,作业帮,高途,跟谁学,学科网,天学网,一起教育,一起作业,美术宝,火花思维,粉笔,51talk,爱学习,高思,老虎国际,一心向上,向上一意,联想,拉勾,乐视,欢聚,竞技世界,拼多多,寻梦,从鲸,TEMU,得物,有赞,Moka,希瑞亚斯,北森,OPPO,欧珀,vivo,维沃,小天才,步步高,读书郎,货拉拉,陌陌,探探,Shopee,虾皮,首汽租车,GoFun,神州租车,天眼查,旷视,小冰,美图,智谱华章,MiniMax,石头科技,迅雷,TP,锐捷,Tenda,腾达,斐讯,希音,SHEIN,稀宇,深言,百川智能,与爱为舞,牵手,Grab,爱回收,洋钱罐,瓴岳,得到,思维造物,地平线,咪咕,翼支付,电信,天翼,联通,蓝湖,墨刀,海尔,美的,米哈游,传音,同花顺,国美,TCL`
|
||||
},
|
||||
{
|
||||
name: '阿里系',
|
||||
value: `阿里,淘宝,淘麦郎,天猫,盒马,口碑,优视,夸克,UC,蚂蚁,飞猪,乌鸫,饿了么,LAZADA,来赞达,菜鸟,哈啰,钉钉,高德,白龙马,新浪,微博`
|
||||
},
|
||||
{
|
||||
name: '字节(头条/抖音)系',
|
||||
value: `抖音,字节,字跳,有竹居,脸萌,头条,懂车帝,巨量引擎`
|
||||
},
|
||||
{
|
||||
name: '百度系',
|
||||
value: `百度,昆仑芯,小度,度小满,爱奇艺,携程,趣拿,去哪儿,集度,作业帮,智图,长地万方,瑞图万方,道道通,小熊博望`
|
||||
},
|
||||
{
|
||||
name: '腾讯系',
|
||||
value: `腾讯,酷狗,酷我,阅文,搜狗,小鹅通,富途,京东,沃东天骏,达达,达冠,美团,三快,猫眼,快手,拼多多,寻梦,从鲸,TEMU,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
|
||||
})
|
||||
}
|
||||
|
||||
const strategyOptionWhenCurrentJobNotMatch = [
|
||||
{
|
||||
name: '在Boss直聘上标记不合适(推荐,这确保Boss直聘可以推荐新职位)',
|
||||
value: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
|
||||
},
|
||||
{
|
||||
name: '本地记录,且1周内再次遇到这个职位时将直接跳过',
|
||||
value: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
|
||||
},
|
||||
{
|
||||
name: '本地不记录,但本次运行再次遇到这个职位时将直接跳过',
|
||||
value: MarkAsNotSuitOp.NO_OP
|
||||
}
|
||||
]
|
||||
|
||||
const noActiveDefinitionMarks = computed(() => {
|
||||
let arr = [...activeDescList]
|
||||
arr.shift()
|
||||
arr.unshift('不处理“职位不活跃”')
|
||||
arr = arr.map((it, index) => {
|
||||
if (index <= formContent.value.markAsNotActiveSelectedTimeRange) {
|
||||
if (formContent.value.markAsNotActiveSelectedTimeRange > 0 && index === 0) {
|
||||
return it
|
||||
}
|
||||
return {
|
||||
style: {
|
||||
color: '#1989FA',
|
||||
fontWeight: 700
|
||||
},
|
||||
label: it
|
||||
}
|
||||
}
|
||||
return it
|
||||
})
|
||||
return arr
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-wrap {
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
:deep(.el-form) {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.last-form-item {
|
||||
:deep(.el-form-item__content) {
|
||||
margin-top: 0px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-active-definition-text-slider {
|
||||
::v-deep(.el-slider__marks-text) {
|
||||
font-size: 10px;
|
||||
margin-top: 5px;
|
||||
&:nth-child(2n-1) {
|
||||
margin-top: 20px;
|
||||
&::before {
|
||||
content: '';
|
||||
bottom: calc(100% - 14px);
|
||||
z-index: -1;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 2px;
|
||||
height: 26px;
|
||||
background-image: linear-gradient(var(--el-slider-runway-bg-color), transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep(.el-slider__stop) {
|
||||
box-shadow: 0 0 0 2px rgba(52, 137, 255, 0.3);
|
||||
}
|
||||
::v-deep(.el-slider__button) {
|
||||
transform: rotate(45deg);
|
||||
border-radius: 10px 10px 0px 10px;
|
||||
--el-slider-button-size: 18px;
|
||||
}
|
||||
}
|
||||
</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>
|
||||
@@ -55,7 +55,17 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getJobLibrary">刷新</el-button>
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('job_library_refresh_clicked')
|
||||
getJobLibrary()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.pageNo"
|
||||
@@ -74,7 +84,13 @@
|
||||
<JobInfoSnapshot
|
||||
v-if="selectedJobInfoForViewSnapshot"
|
||||
:job-info="selectedJobInfoForViewSnapshot"
|
||||
@closed="selectedJobInfoForViewSnapshot = null"
|
||||
scene="jobLibrary"
|
||||
@closed="
|
||||
() => {
|
||||
gtagRenderer('job_info_snapshot_closed')
|
||||
selectedJobInfoForViewSnapshot = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ElDrawer>
|
||||
<ElDialog
|
||||
@@ -92,6 +108,7 @@
|
||||
:job-info-history-list="selectedJobHistory ?? []"
|
||||
@closed="
|
||||
() => {
|
||||
gtagRenderer('job_library_list_dialog_closed')
|
||||
selectedJobInfoForViewHistory = null
|
||||
selectedJobHistory = null
|
||||
}
|
||||
@@ -109,6 +126,7 @@ import { type JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/src/entity/Job
|
||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||
import JobInfoHistoryList from '../../features/JobInfoHistoryList/index.vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VChatStartupLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -121,6 +139,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getJobLibrary() {
|
||||
try {
|
||||
gtagRenderer('job_library_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-job-library', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -132,7 +154,16 @@ async function getJobLibrary() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('job_library_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('job_library_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
@@ -160,10 +191,12 @@ const drawVisibleModelValue = ref(false)
|
||||
const selectedJobInfoForViewSnapshot = ref<VChatStartupLog | null>(null)
|
||||
|
||||
function handleViewJobSnapshotButtonClick(record: VChatStartupLog) {
|
||||
gtagRenderer('view_job_snapshot_button_clicked')
|
||||
selectedJobInfoForViewSnapshot.value = record
|
||||
drawVisibleModelValue.value = true
|
||||
}
|
||||
async function handleViewJobOnlineButtonClick(encryptJobId: string) {
|
||||
gtagRenderer('view_job_online_button_clicked')
|
||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||
url: `https://www.zhipin.com/job_detail/${encryptJobId}.html`
|
||||
})
|
||||
@@ -173,6 +206,7 @@ const historyDialogVisibleModelValue = ref(false)
|
||||
const selectedJobInfoForViewHistory = ref<VChatStartupLog | null>(null)
|
||||
const selectedJobHistory = ref<null | JobInfoChangeLog[]>(null)
|
||||
async function handleViewJobHistoryButtonClick(record: VChatStartupLog) {
|
||||
gtagRenderer('view_job_history_button_clicked')
|
||||
let { data: historyList } = await electron.ipcRenderer.invoke(
|
||||
'get-job-history-by-encrypt-id',
|
||||
record.encryptJobId
|
||||
@@ -227,12 +261,13 @@ async function handleViewJobHistoryButtonClick(record: VChatStartupLog) {
|
||||
// })
|
||||
|
||||
if (!historyList.length) {
|
||||
gtagRenderer('job_history_is_not_found')
|
||||
ElMessage.warning({
|
||||
message: '未找到与此条目相关的历史变更记录,再多投一投吧'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
gtagRenderer('job_history_is_found')
|
||||
historyDialogVisibleModelValue.value = true
|
||||
selectedJobInfoForViewHistory.value = record
|
||||
selectedJobHistory.value = historyList
|
||||
@@ -72,7 +72,15 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getMarkAsNotSuitRecord"
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('mansr_refresh_clicked')
|
||||
getMarkAsNotSuitRecord()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -93,7 +101,13 @@
|
||||
<JobInfoSnapshot
|
||||
v-if="selectedJobInfoForViewSnapshot"
|
||||
:job-info="selectedJobInfoForViewSnapshot"
|
||||
@closed="selectedJobInfoForViewSnapshot = null"
|
||||
scene="markAsNotSuitRecord"
|
||||
@closed="
|
||||
() => {
|
||||
gtagRenderer('mansr_closed')
|
||||
selectedJobInfoForViewSnapshot = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
@@ -107,6 +121,7 @@ import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/src/enums'
|
||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VMarkAsNotSuitLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -122,6 +137,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getMarkAsNotSuitRecord() {
|
||||
try {
|
||||
gtagRenderer('mansr_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-mark-as-not-suit-record', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -133,7 +152,16 @@ async function getMarkAsNotSuitRecord() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('mansr_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('mansr_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
@@ -158,6 +186,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
async function handleViewJobOnlineButtonClick(encryptJobId: string) {
|
||||
gtagRenderer('view_job_online_button_clicked')
|
||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||
url: `https://www.zhipin.com/job_detail/${encryptJobId}.html`
|
||||
})
|
||||
@@ -167,6 +196,7 @@ const drawVisibleModelValue = ref(false)
|
||||
const selectedJobInfoForViewSnapshot = ref<VMarkAsNotSuitLog | null>(null)
|
||||
|
||||
function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) {
|
||||
gtagRenderer('view_job_snapshot_button_clicked')
|
||||
selectedJobInfoForViewSnapshot.value = record
|
||||
drawVisibleModelValue.value = true
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
<template>
|
||||
<div class="form-wrap">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:rules="formRules"
|
||||
:model="formContent.autoReminder"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item label="BOSS直聘 Cookie">
|
||||
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
|
||||
>编辑Cookie</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div>
|
||||
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
|
||||
发送提醒消息前,先按照“Boss炸弹-职位类型正则”校验正在与Boss沟通的岗位是否满足期望,校验通过后再提醒
|
||||
</el-checkbox>
|
||||
<template v-else>
|
||||
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithExpectJobType">
|
||||
发送提醒消息前,先按照“Boss炸弹-职位类型正则”校验正在与Boss沟通的岗位是否满足期望,校验通过后再提醒
|
||||
</el-checkbox>
|
||||
<div ml1.5em color-gray>
|
||||
<div>当前职位类型正则:{{ expectJobTypeRegExpStr?.trim() }}</div>
|
||||
<template
|
||||
v-if="
|
||||
formContent.autoReminder.rechatContentSource ===
|
||||
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
|
||||
"
|
||||
>
|
||||
<div>当前简历中填写的期望职位:{{ resumeContent?.expectJob ?? '-' }}</div>
|
||||
<div color-orange>请确保上方二者信息匹配</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的Boss时,将要向Boss发出:">
|
||||
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
|
||||
<div>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
placement="right"
|
||||
:enterable="false"
|
||||
@show="gtagRenderer('tooltip_show_about_lfr_emotion_figure')"
|
||||
>
|
||||
<template #content>
|
||||
<img block h-100px src="./resources/look-forward-reply-emotion.gif" />
|
||||
</template>
|
||||
<el-radio :label="RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION">
|
||||
“[盼回复]” 表情
|
||||
</el-radio>
|
||||
</el-tooltip>
|
||||
<br />
|
||||
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
|
||||
由大语言模型(根据简历及当前聊天上下文)生成的内容
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div class="ml-30px">
|
||||
<template
|
||||
v-if="
|
||||
formContent.autoReminder.rechatContentSource ===
|
||||
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
|
||||
"
|
||||
>
|
||||
<el-form-item class="mb4px">
|
||||
<div>
|
||||
<el-button size="small" type="primary" @click="handleClickConfigLlm">
|
||||
配置大语言模型
|
||||
</el-button>
|
||||
<div class="font-size-12px color-#666">
|
||||
支持
|
||||
<span
|
||||
class="pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
|
||||
style="background-color: #3c4efd"
|
||||
>DeepSeek-V3</span
|
||||
>
|
||||
<span
|
||||
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
|
||||
style="background-color: #000000"
|
||||
>GPT-4o mini</span
|
||||
>
|
||||
<span
|
||||
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
|
||||
style="background-color: #462ac4"
|
||||
>Qwen2.5</span
|
||||
>
|
||||
模型;支持多个“服务商-模型”组合按权重搭配使用
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item class="mb4px">
|
||||
<div>
|
||||
<el-button size="small" type="primary" @click="handleClickEditResume">
|
||||
编辑简历
|
||||
</el-button>
|
||||
<div class="font-size-12px color-#666">
|
||||
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item class="mb4px">
|
||||
<div>
|
||||
<div>
|
||||
<el-button size="small" type="primary" @click="handleClickEditPrompt">
|
||||
使用外部编辑器编辑提示词模板 (Markdown)
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('reset_template_clicked_in_main_form')
|
||||
restoreDefaultTemplate()
|
||||
}
|
||||
"
|
||||
>
|
||||
还原默认提示词模板
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="font-size-12px color-#666">
|
||||
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入
|
||||
__REPLACE_REAL_RESUME_HERE__
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="recentMessageQuantityForLlm">
|
||||
<div>
|
||||
携带最近
|
||||
<el-input-number
|
||||
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
|
||||
class="w-120px"
|
||||
:min="8"
|
||||
:max="20"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
></el-input-number>
|
||||
次聊天内容作为上下文生成新消息
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="small" type="primary" @click="handleTestEffectClicked"
|
||||
>使用当前配置模拟已读不回复聊过程</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item prop="recentMessageQuantityForLlm">
|
||||
<div class="flex flex-items-center">
|
||||
<span class="whitespace-nowrap">当所有模型均不可使用时 </span>
|
||||
<el-select
|
||||
v-model="formContent.autoReminder.rechatLlmFallback"
|
||||
class="w200px"
|
||||
label="name"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in rechatLlmFallbackOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:label="option.name"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
|
||||
<el-input-number
|
||||
v-model="formContent.autoReminder.throttleIntervalMinutes"
|
||||
class="w-150px"
|
||||
:min="3"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
@blur="handleThrottleIntervalMinutesBlur"
|
||||
/> 分钟内不多次跟进同一Boss
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进时限(天)" prop="rechatLimitDay">
|
||||
<div>
|
||||
<div><el-checkbox v-model="enableRechatLimit" /> 启用</div>
|
||||
<el-input-number
|
||||
v-model="formContent.autoReminder.rechatLimitDay"
|
||||
class="w-150px"
|
||||
:min="0"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
:disabled="!enableRechatLimit"
|
||||
/> 天<br />
|
||||
<div v-if="enableRechatLimit">
|
||||
不再跟进 (<span class="text-orange">{{ rechatLimitDateString }}</span
|
||||
>)之前列表中没有进展的聊天
|
||||
</div>
|
||||
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span>)</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item class="last-form-item">
|
||||
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { dayjs, ElForm, ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
RECHAT_CONTENT_SOURCE,
|
||||
RECHAT_LLM_FALLBACK
|
||||
} from '../../../../common/enums/auto-start-chat'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import mittBus from '../../utils/mitt'
|
||||
|
||||
const router = useRouter()
|
||||
const formContent = ref({
|
||||
autoReminder: {
|
||||
throttleIntervalMinutes: 10,
|
||||
rechatLimitDay: 21,
|
||||
rechatContentSource: 1,
|
||||
recentMessageQuantityForLlm: 8,
|
||||
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION,
|
||||
onlyRemindBossWithExpectJobType: true
|
||||
}
|
||||
})
|
||||
|
||||
const enableRechatLimit = computed({
|
||||
get() {
|
||||
return Boolean(formContent.value.autoReminder?.rechatLimitDay)
|
||||
},
|
||||
set(val) {
|
||||
if (!val) {
|
||||
gtagRenderer('rechat_limit_disabled')
|
||||
formContent.value.autoReminder.rechatLimitDay = 0
|
||||
} else {
|
||||
gtagRenderer('rechat_limit_enabled')
|
||||
formContent.value.autoReminder.rechatLimitDay = 21
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
|
||||
const conf = res.config['boss.json']?.autoReminder || {}
|
||||
conf.throttleIntervalMinutes = conf.throttleIntervalMinutes ?? 10
|
||||
conf.rechatLimitDay = conf.rechatLimitDay ?? 21
|
||||
conf.rechatContentSource = conf.rechatContentSource ?? 1
|
||||
conf.recentMessageQuantityForLlm =
|
||||
typeof conf.recentMessageQuantityForLlm === 'number'
|
||||
? conf.recentMessageQuantityForLlm > 20
|
||||
? 20
|
||||
: conf.recentMessageQuantityForLlm < 8
|
||||
? 8
|
||||
: parseInt(conf.recentMessageQuantityForLlm)
|
||||
: 8
|
||||
conf.onlyRemindBossWithExpectJobType = conf.onlyRemindBossWithExpectJobType ?? true
|
||||
conf.rechatLlmFallback = conf.rechatLlmFallback ?? RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||
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 = {
|
||||
throttleIntervalMinutes: {
|
||||
validator(_, value, cb) {
|
||||
if (/[^0-9.]/.test(String(value)) || isNaN(parseFloat(value)) || isNaN(Number(value))) {
|
||||
cb(new Error(`请输入数字!`))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
},
|
||||
rechatLimitDay: {
|
||||
validator(_, value, cb) {
|
||||
if (/[^0-9.]/.test(String(value)) || isNaN(parseFloat(value)) || isNaN(Number(value))) {
|
||||
cb(new Error(`请输入数字!`))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
watch(
|
||||
() => formContent.value.autoReminder,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
formRef.value?.validate?.()
|
||||
})
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
async function checkIsCanRun() {
|
||||
if (!(await electron.ipcRenderer.invoke('check-is-resume-content-valid'))) {
|
||||
gtagRenderer('cannot_launch_for_invalid_rc_dialog_show')
|
||||
try {
|
||||
await ElMessageBox.confirm(`简历内容无效;您需要编辑一下您的简历`, {
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonText: '好的,去编辑我的简历',
|
||||
dangerouslyUseHTMLString: true
|
||||
})
|
||||
gtagRenderer('invalid_rc_dialog_click_confirm')
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('resume-edit')
|
||||
await fetchResumeContent()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
} catch {
|
||||
gtagRenderer('invalid_rc_dialog_click_cancel')
|
||||
}
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('check-if-llm-config-list-valid')
|
||||
} catch (err) {
|
||||
if (err?.message?.includes(`CANNOT_FIND_VALID_CONFIG`)) {
|
||||
gtagRenderer('cannot_launch_for_invalid_llm_config')
|
||||
console.log(`大模型配置无效`, err)
|
||||
ElMessageBox.confirm(
|
||||
'大模型配置不存在或者包含无效配置<br />您是否希望查看并修正当前大模型配置?',
|
||||
'',
|
||||
{
|
||||
confirmButtonText: '是',
|
||||
cancelButtonText: '否',
|
||||
type: 'warning',
|
||||
closeOnClickModal: false,
|
||||
dangerouslyUseHTMLString: true
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
gtagRenderer('invalid_llm_config_tip_dialog_confirm')
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('llm-config')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
gtagRenderer('invalid_llm_config_tip_dialog_cancel')
|
||||
})
|
||||
} else {
|
||||
gtagRenderer('cannot_launch_for_check_llm_config_error', { err })
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '大模型配置检查未通过,请重试'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid')
|
||||
} catch (err) {
|
||||
if (err?.message?.includes(`RESUME_PLACEHOLDER_NOT_EXIST`)) {
|
||||
gtagRenderer('cannot_launch_for_no_resume_placehold')
|
||||
console.log(`提示词模板无效`, err)
|
||||
ElMessageBox.confirm(
|
||||
'提示词模板缺少简历内容占位符:<br /><b>__REPLACE_REAL_RESUME_HERE__</b><br /><br />您是否希望还原默认的提示词模板?',
|
||||
'',
|
||||
{
|
||||
confirmButtonText: '是',
|
||||
cancelButtonText: '否',
|
||||
type: 'warning',
|
||||
closeOnClickModal: false,
|
||||
dangerouslyUseHTMLString: true
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
gtagRenderer('confirm_invalid_rt_tip_dialog')
|
||||
await restoreDefaultTemplate()
|
||||
})
|
||||
.catch(() => {
|
||||
gtagRenderer('close_invalid_rt_tip_dialog')
|
||||
})
|
||||
} else {
|
||||
gtagRenderer('cannot_launch_for_check_prompt_error', { err })
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '用于生成自动提醒消息的提示词检查未通过,请重试'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
gtagRenderer('run_read_no_reply_reminder_clicked', {
|
||||
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
|
||||
rechat_limit_day: formContent.value.autoReminder.rechatLimitDay,
|
||||
rechat_content_source: formContent.value.autoReminder.rechatContentSource,
|
||||
recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm
|
||||
})
|
||||
await formRef.value!.validate()
|
||||
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
|
||||
gtagRenderer('config_saved')
|
||||
if (
|
||||
formContent.value.autoReminder?.rechatContentSource ===
|
||||
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
|
||||
) {
|
||||
if (!(await checkIsCanRun())) {
|
||||
return
|
||||
}
|
||||
if (!(await electron.ipcRenderer.invoke('resume-content-enough-detect'))) {
|
||||
gtagRenderer('rc_not_enough_dialog_show')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`简历内容可能不够充足(各个部分内容长度相加 <800 字)<br />后续大模型根据简历生成的内容将可能不符合预期(例如相同内容重复生成、生成预期之外的内容)<br /><br />要继续运行吗?`,
|
||||
{
|
||||
cancelButtonText: '不,我再看看',
|
||||
confirmButtonText: '是的,继续运行',
|
||||
dangerouslyUseHTMLString: true
|
||||
}
|
||||
)
|
||||
gtagRenderer('rc_not_enough_dialog_click_confirm')
|
||||
} catch {
|
||||
gtagRenderer('rc_not_enough_dialog_click_cancel')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
gtagRenderer('run_read_no_reply_reminder_launched')
|
||||
router.replace({
|
||||
path: '/geekAutoStartChatWithBoss/prepareRun',
|
||||
query: { flow: 'read-no-reply-reminder' }
|
||||
})
|
||||
}
|
||||
function handleThrottleIntervalMinutesBlur() {
|
||||
if (formContent.value.autoReminder.throttleIntervalMinutes < 3) {
|
||||
formContent.value.autoReminder.throttleIntervalMinutes = 3
|
||||
}
|
||||
formContent.value.autoReminder.throttleIntervalMinutes = Number(
|
||||
formContent.value.autoReminder.throttleIntervalMinutes
|
||||
)
|
||||
}
|
||||
|
||||
const restoreDefaultTemplate = async () => {
|
||||
await electron.ipcRenderer.invoke('overwrite-auto-remind-prompt-with-default')
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '模板还原成功'
|
||||
})
|
||||
}
|
||||
|
||||
const handleClickLaunchLogin = () => {
|
||||
gtagRenderer('launch_login_clicked')
|
||||
router.replace('/cookieAssistant')
|
||||
}
|
||||
|
||||
const currentStamp = ref(new Date())
|
||||
let timer = 0
|
||||
function updateCurrentStamp() {
|
||||
currentStamp.value = new Date()
|
||||
timer = window.setTimeout(updateCurrentStamp, 1000)
|
||||
}
|
||||
updateCurrentStamp()
|
||||
onUnmounted(() => {
|
||||
window.clearTimeout(timer)
|
||||
})
|
||||
|
||||
const rechatLimitDateString = computed(() => {
|
||||
return dayjs(
|
||||
+currentStamp.value - formContent.value.autoReminder.rechatLimitDay * 24 * 60 * 60 * 1000
|
||||
).format('YYYY-MM-DD HH:mm:ss')
|
||||
})
|
||||
|
||||
const handleClickConfigLlm = async () => {
|
||||
gtagRenderer('config_llm_clicked')
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('llm-config')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickEditResume = async () => {
|
||||
gtagRenderer('edit_resume_clicked')
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('resume-edit')
|
||||
await fetchResumeContent()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickEditPrompt = async () => {
|
||||
gtagRenderer('edit_prompt_clicked')
|
||||
await electron.ipcRenderer.send('no-reply-reminder-prompt-edit')
|
||||
}
|
||||
|
||||
const rechatLlmFallbackOptions = [
|
||||
{
|
||||
name: '发送“[盼回复]”表情',
|
||||
value: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||
},
|
||||
{
|
||||
name: '退出已读不回提醒器',
|
||||
value: RECHAT_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
|
||||
}
|
||||
]
|
||||
|
||||
async function handleTestEffectClicked() {
|
||||
gtagRenderer('goto_mock_chat_clicked')
|
||||
if (!(await checkIsCanRun())) {
|
||||
return
|
||||
}
|
||||
electron.ipcRenderer.send('test-llm-config-effect', {
|
||||
autoReminderConfig: JSON.parse(JSON.stringify(formContent.value.autoReminder))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-wrap {
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
:deep(.el-form) {
|
||||
margin: 0 auto;
|
||||
max-width: 1000px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.last-form-item {
|
||||
:deep(.el-form-item__content) {
|
||||
margin-top: 0px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -55,7 +55,15 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getAutoStartChatRecord"
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('start_chat_record_refresh_clicked')
|
||||
getAutoStartChatRecord()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -76,7 +84,13 @@
|
||||
<JobInfoSnapshot
|
||||
v-if="selectedJobInfoForViewSnapshot"
|
||||
:job-info="selectedJobInfoForViewSnapshot"
|
||||
@closed="selectedJobInfoForViewSnapshot = null"
|
||||
scene="startChatRecord"
|
||||
@closed="
|
||||
() => {
|
||||
gtagRenderer('start_chat_record_closed')
|
||||
selectedJobInfoForViewSnapshot = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
@@ -89,6 +103,7 @@ import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VCha
|
||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VChatStartupLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -104,6 +119,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getAutoStartChatRecord() {
|
||||
try {
|
||||
gtagRenderer('start_chat_record_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-auto-start-chat-record', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -115,7 +134,16 @@ async function getAutoStartChatRecord() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('start_chat_record_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('start_chat_record_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
258
packages/ui/src/renderer/src/page/MainLayout/index.vue
Normal file
258
packages/ui/src/renderer/src/page/MainLayout/index.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div class="flex h100vh">
|
||||
<div class="flex flex-col w200px pt30px pl30px aside-nav of-hidden">
|
||||
<div class="nav-list flex-1 of-auto">
|
||||
<RouterLink to="./GeekAutoStartChatWithBoss">
|
||||
Boss炸弹
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:enterable="false"
|
||||
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
|
||||
>
|
||||
<template #content>
|
||||
<div w-480px>
|
||||
<div>扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。</div>
|
||||
<br />
|
||||
<div>匹配步骤</div>
|
||||
<ol m0 pl2em>
|
||||
<li>
|
||||
按照公司名称查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
|
||||
</li>
|
||||
<li>
|
||||
检查Boss活跃度
|
||||
<ul pl2em>
|
||||
<li>
|
||||
如果Boss活跃度为本月活跃或更往前的时间,则会把职位标记为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
对职位名称、职位类型、职位描述进行匹配
|
||||
<ul pl2em>
|
||||
<li>如果匹配则自动点击开聊按钮</li>
|
||||
<li>
|
||||
不匹配则标记这个职位为不合适,一段时间内你将不会在Boss上看到这个职位,且将会推荐新职位置换这个职位
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<br />
|
||||
<div>异常情况</div>
|
||||
<ol m0 pl2em>
|
||||
<li>
|
||||
当前页面筛选条件下,如果没有更多职位,则自动切换备选筛选条件,以获取更多新职位
|
||||
</li>
|
||||
<li>
|
||||
如当天开聊次数用完,本程序会暂停运行60分钟,之后尝试继续重新运行;如重新运行时间已在第二天,则将会继续开聊
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
<QuestionFilled w-1em h-1em mr10px />
|
||||
</el-tooltip>
|
||||
</RouterLink>
|
||||
<RouterLink to="./ReadNoReplyReminder">
|
||||
已读不回提醒器
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:enterable="false"
|
||||
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
|
||||
>
|
||||
<template #content>
|
||||
<div w-480px>
|
||||
<div>
|
||||
Boss不明原因已读不回?简历就是投不出去?<br />
|
||||
已读不回提醒器,有事没事提醒一下已读不回的 Ta,助力把握每次机会
|
||||
</div>
|
||||
<br />
|
||||
<div>匹配逻辑</div>
|
||||
<div>在聊天列表中查找对你消息已读不回的Boss,再发一条消息,多次复聊;同时:</div>
|
||||
<ul m0 pl2em>
|
||||
<li>如果设置了“跟进时限”,那么在这个时间之前活跃的聊天将不会被检查</li>
|
||||
<li>
|
||||
如果设置了“跟进间隔”,且再次检查时发现Boss已读不回,且距离上次提醒时间间隔小于这个时间,那么聊天将暂时不会跟进,直到下次检查时距离上次提醒时间间隔大于这个时间
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<div>发送内容</div>
|
||||
<ul m0 pl2em>
|
||||
<li>“[盼回复]”表情</li>
|
||||
<li>由大语言模型(根据简历及当前聊天上下文)生成的内容</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<QuestionFilled w-1em h-1em mr10px />
|
||||
</el-tooltip>
|
||||
</RouterLink>
|
||||
<hr w180px />
|
||||
<a href="javascript:void(0)" @click="handleLaunchBossSite">
|
||||
手动逛Boss<TopRight w-1em h-1em mr10px />
|
||||
</a>
|
||||
<hr w180px />
|
||||
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
|
||||
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
|
||||
<RouterLink to="./JobLibrary">职位库</RouterLink>
|
||||
<RouterLink to="./BossLibrary">Boss库</RouterLink>
|
||||
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
|
||||
</div>
|
||||
<div class="pt-16px pb-16px flex-0 font-size-12px">
|
||||
<div v-if="updateStore.availableNewRelease" mb16px>
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}"
|
||||
>
|
||||
最新版本: {{ updateStore.availableNewRelease.releaseVersion }}
|
||||
<img
|
||||
h12px
|
||||
ml10px
|
||||
:style="{
|
||||
filter: `saturate(1.5) brightness(1.5)`,
|
||||
transform: `translateY(-10px)`
|
||||
}"
|
||||
src="./resources/new.gif"
|
||||
/>
|
||||
</div>
|
||||
<div class="update-button-area flex flex-items-center mt-8px">
|
||||
<el-button type="text" size="small" @click="handleDownloadNewReleaseClick"
|
||||
>从GitHub下载</el-button
|
||||
>
|
||||
|
|
||||
<el-button type="text" size="small" @click="handleViewNewReleaseClick"
|
||||
>了解更新内容</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>当前版本: {{ buildInfo.version }}({{ buildInfo.buildVersion }})</div>
|
||||
<div class="feedback-button-area flex flex-items-center mt-8px">
|
||||
<el-button type="text" size="small" @click="handleGotoProjectPageClick"
|
||||
>项目首页</el-button
|
||||
>
|
||||
|
|
||||
<el-button type="text" size="small" @click="handleFeedbackClick">反馈问题</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RouterView v-slot="{ Component }" class="flex-1">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
|
||||
import useBuildInfo from '@renderer/hooks/useBuildInfo'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { useUpdateStore } from '../../store/index'
|
||||
|
||||
const router = useRouter()
|
||||
const unmountedCbs: Array<InstanceType<typeof Function>> = []
|
||||
onUnmounted(() => {
|
||||
while (unmountedCbs.length) {
|
||||
const fn = unmountedCbs.shift()!
|
||||
try {
|
||||
fn()
|
||||
} catch {}
|
||||
}
|
||||
})
|
||||
const goToCheckBossZhipinCookieFile = () => router.replace('/cookieAssistant')
|
||||
onMounted(() => {
|
||||
electron.ipcRenderer.on('check-boss-zhipin-cookie-file', goToCheckBossZhipinCookieFile)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
electron.ipcRenderer.removeListener(
|
||||
'check-boss-zhipin-cookie-file',
|
||||
goToCheckBossZhipinCookieFile
|
||||
)
|
||||
})
|
||||
;(async () => {
|
||||
const checkDependenciesResult = await electron.ipcRenderer.invoke('check-dependencies')
|
||||
if (Object.values(checkDependenciesResult).includes(false)) {
|
||||
router.replace('/')
|
||||
return
|
||||
}
|
||||
|
||||
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
|
||||
if (!isCookieFileValid) {
|
||||
router.replace('/cookieAssistant')
|
||||
return
|
||||
}
|
||||
})()
|
||||
|
||||
const { buildInfo } = useBuildInfo()
|
||||
const handleFeedbackClick = () => {
|
||||
gtagRenderer('goto_feedback_clicked')
|
||||
electron.ipcRenderer.send('send-feed-back-to-github-issue')
|
||||
}
|
||||
const handleGotoProjectPageClick = () => {
|
||||
gtagRenderer('goto_project_github_clicked')
|
||||
electron.ipcRenderer.send('open-external-link', 'https://github.com/geekgeekrun/geekgeekrun')
|
||||
}
|
||||
|
||||
const handleLaunchBossSite = debounce(
|
||||
async () => {
|
||||
gtagRenderer('launch_boss_site_clicked')
|
||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||
url: `https://www.zhipin.com/`
|
||||
})
|
||||
},
|
||||
1000,
|
||||
{ leading: true, trailing: false }
|
||||
)
|
||||
|
||||
const updateStore = useUpdateStore()
|
||||
function handleDownloadNewReleaseClick() {
|
||||
gtagRenderer('click_download_release_form_nav')
|
||||
electron.ipcRenderer.send('open-external-link', updateStore.availableNewRelease!.assetUrl)
|
||||
}
|
||||
function handleViewNewReleaseClick() {
|
||||
gtagRenderer('click_view_release_form_nav')
|
||||
electron.ipcRenderer.send('open-external-link', updateStore.availableNewRelease!.releasePageUrl)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.aside-nav {
|
||||
background-image: linear-gradient(45deg, #eaf4f1, #dcf6f2);
|
||||
.nav-list {
|
||||
> a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 2.5em;
|
||||
box-sizing: border-box;
|
||||
padding-left: 2em;
|
||||
&.router-link-active {
|
||||
background-color: #fff;
|
||||
font-weight: 700;
|
||||
color: #2faa9e;
|
||||
border-radius: 9999px 0 0 9999px;
|
||||
}
|
||||
}
|
||||
> hr {
|
||||
border: 0 solid;
|
||||
height: 1px;
|
||||
background-color: #b3c8c3;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.feedback-button-area,
|
||||
.update-button-area {
|
||||
:deep(.el-button) {
|
||||
height: fit-content;
|
||||
padding: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 499 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 512 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
packages/ui/src/renderer/src/page/MainLayout/resources/new.gif
Normal file
BIN
packages/ui/src/renderer/src/page/MainLayout/resources/new.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div class="h100vh flex flex-col">
|
||||
<div
|
||||
ref="scrollElRef"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
overflow: `auto`,
|
||||
margin: `0 auto`,
|
||||
alignItems: `flex-end`,
|
||||
width: '100%'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="messageList.length"
|
||||
:style="{
|
||||
width: '480px',
|
||||
margin: '0 auto'
|
||||
}"
|
||||
>
|
||||
<div class="pb20px"></div>
|
||||
<div v-for="(item, index) in messageList" :key="index" flex flex-col flex-items-end>
|
||||
<div class="message-item-wrap flex flex-col">
|
||||
<div
|
||||
class="message-item"
|
||||
:class="{
|
||||
'will-enter-context': getIsEnterContent(index)
|
||||
}"
|
||||
>
|
||||
{{ item.text }}
|
||||
</div>
|
||||
<div
|
||||
:style="{
|
||||
width: 'fit-content',
|
||||
alignSelf: 'flex-end'
|
||||
}"
|
||||
font-size-10px
|
||||
>
|
||||
{{ item.usedLlmConfig.model }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item?.usedLlmConfig?.providerCompleteApiUrl?.trim()"
|
||||
:style="{
|
||||
width: 'fit-content',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
alignSelf: 'flex-end',
|
||||
color: '#bbb'
|
||||
}"
|
||||
font-size-10px
|
||||
w-fit-content
|
||||
max-w-20em
|
||||
>
|
||||
{{ item.usedLlmConfig.providerCompleteApiUrl }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pb20px"></div>
|
||||
</div>
|
||||
<div v-else w-full h-full flex flex-item-center justify-center>
|
||||
<el-empty>
|
||||
<template #description>
|
||||
<template v-if="!isLoading">
|
||||
点击下方 “<el-button
|
||||
font-size-16px
|
||||
h-fit-content
|
||||
align-baseline
|
||||
p0
|
||||
type="text"
|
||||
@click.prevent="sendLlmGeneratedContent"
|
||||
>发送开场白</el-button
|
||||
>” 以开始模拟聊天
|
||||
</template>
|
||||
<template v-else>请稍候,第一条消息正在回复的路上~</template>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'min-content 1fr min-content',
|
||||
height: `fit-content`,
|
||||
paddingTop: `10px`,
|
||||
paddingBottom: `10px`,
|
||||
backgroundColor: `#f0f0f0`
|
||||
}"
|
||||
>
|
||||
<el-select v-model="selectedLlmConfig" ml10px w160px placeholder="随机使用一个模型">
|
||||
<el-option
|
||||
v-for="(it, index) in llmConfigListForRender"
|
||||
:key="index"
|
||||
:value="it.id"
|
||||
:label="it.model"
|
||||
:disabled="!it.enabled"
|
||||
:style="{
|
||||
paddingTop: '10px',
|
||||
paddingBottom: '10px',
|
||||
height: 'auto',
|
||||
lineHeight: '1.25em'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
}"
|
||||
>
|
||||
<div>{{ it.model }}</div>
|
||||
<div class="font-size-12px color-#bbb">
|
||||
{{ formatApiSecret(it.providerApiSecret) || '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="it?.providerCompleteApiUrl?.trim?.()"
|
||||
:style="{
|
||||
color: '#bbb',
|
||||
width: '35em',
|
||||
fontSize: '12px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}"
|
||||
>
|
||||
{{ it.providerCompleteApiUrl }}
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button
|
||||
:loading="isLoading"
|
||||
width-fit-content
|
||||
type="primary"
|
||||
@click="sendLlmGeneratedContent"
|
||||
>
|
||||
<template v-if="isLoading">正在生成消息,请稍候...</template>
|
||||
<template v-else-if="!messageList.length">发送开场白</template>
|
||||
<template v-else>发送下一句提醒内容</template>
|
||||
</el-button>
|
||||
<el-button mr10px type="text" @click="closeWindow">关闭对话框</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
type MessageItem = {
|
||||
text: string
|
||||
usedLlmConfig: string
|
||||
// recordInfo: any
|
||||
}
|
||||
const messageList = ref<MessageItem[]>([])
|
||||
|
||||
const recentMessageQuantityForLlm =
|
||||
Number(new URL(location.href).searchParams.get('recentMessageQuantityForLlm')) || 8
|
||||
function getIsEnterContent(index) {
|
||||
return messageList.value.length - index - 1 < recentMessageQuantityForLlm
|
||||
}
|
||||
|
||||
const llmConfigList = ref([])
|
||||
const llmConfigListForRender = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: null,
|
||||
model: '随机使用一个模型',
|
||||
providerCompleteApiUrl: null,
|
||||
enabled: true
|
||||
},
|
||||
...(llmConfigList.value ?? [])
|
||||
]
|
||||
})
|
||||
async function getLlmConfigList() {
|
||||
llmConfigList.value = await electron.ipcRenderer.invoke('get-llm-config-for-test')
|
||||
}
|
||||
getLlmConfigList().catch(() => {})
|
||||
const selectedLlmConfig = ref(null)
|
||||
watch(
|
||||
() => selectedLlmConfig.value,
|
||||
() => {
|
||||
gtagRenderer('change_mock_chat_llm_model', {
|
||||
model: selectedLlmConfig.value?.model ?? ''
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const scrollElRef = ref(null)
|
||||
const isLoading = ref(false)
|
||||
async function sendLlmGeneratedContent() {
|
||||
gtagRenderer('click_mock_chat_send')
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
|
||||
messageList: JSON.parse(JSON.stringify((messageList.value ?? []).slice(-8))),
|
||||
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
|
||||
})
|
||||
console.log(response)
|
||||
messageList.value.push({
|
||||
text: response.responseText,
|
||||
usedLlmConfig: response.usedLlmConfig
|
||||
})
|
||||
await sleep(50)
|
||||
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
|
||||
top: scrollElRef.value?.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
} catch (err) {
|
||||
ElMessage.error({
|
||||
dangerouslyUseHTMLString: true,
|
||||
grouping: true,
|
||||
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
electron.ipcRenderer.send(`close-read-no-reply-reminder-llm-mock-window`)
|
||||
}
|
||||
|
||||
function formatApiSecret(text) {
|
||||
if (typeof text !== 'string' || !text?.trim()) {
|
||||
return ''
|
||||
}
|
||||
if (text === 'ollama') {
|
||||
return text
|
||||
}
|
||||
if (text.length >= 8) {
|
||||
return `${text.slice(0, 4)}***${text.slice(-4)}`
|
||||
}
|
||||
return `***`
|
||||
}
|
||||
|
||||
gtagRenderer('enter_mock_chat_page')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-item-wrap {
|
||||
max-width: 420px;
|
||||
margin-top: 20px;
|
||||
.message-item {
|
||||
line-height: 1.25em;
|
||||
font-size: 14px;
|
||||
background-color: #d1f0ef;
|
||||
color: #333;
|
||||
padding: 10px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
&.will-enter-context {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '聊天上下文';
|
||||
display: flex;
|
||||
font-size: 10px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: #10c7c3;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
564
packages/ui/src/renderer/src/page/ResumeEditor/index.vue
Normal file
564
packages/ui/src/renderer/src/page/ResumeEditor/index.vue
Normal file
@@ -0,0 +1,564 @@
|
||||
<template>
|
||||
<div class="resume-editor-page">
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
<div class="mt1em mb1em flex flex-items-center flex-justify-between">
|
||||
<span>简历编辑器</span>
|
||||
</div>
|
||||
<el-alert type="info" :closable="false" mb20px line-height-1.25em>
|
||||
<ul pl16px m0>
|
||||
<li>
|
||||
此简历将作为提示词的一部分提交给语言大模型,仅在匹配职位、生成已读不回提醒消息时使用;大部分信息非必填,但在不填写的情况下,可能会匹配到不准确的职位或生成预料之外的已读不回提醒消息
|
||||
</li>
|
||||
<li>期望薪资仅作匹配职位使用,不会用作生成已读不回提醒消息</li>
|
||||
</ul>
|
||||
</el-alert>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formContentForElForm"
|
||||
:rules="formRulesForElForm"
|
||||
label-position="top"
|
||||
class="resume-editor-form"
|
||||
:validate-on-rule-change="false"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '10px'
|
||||
}"
|
||||
>
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="formContent.name" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="工作年限">
|
||||
<el-input v-model="formContent.workYearDesc" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="期望职位">
|
||||
<el-input v-model="formContent.expectJob" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="期望薪资(k)">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-input v-model="formContent.expectSalary[0]" placeholder="下限" />
|
||||
<el-input v-model="formContent.expectSalary[1]" placeholder="上限" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item label="个人优势">
|
||||
<el-input
|
||||
v-model="formContent.userDescription"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="el-form-item__label">
|
||||
工作经历
|
||||
<el-button size="small" :icon="Plus" @click="addWorkExp">新增一条</el-button>
|
||||
</div>
|
||||
<div v-for="(exp, index) in formContent.geekWorkExpList" :key="index">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
gap: '12px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
height: 'fit-content',
|
||||
marginTop: '10px'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveWorkExpUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= formContent.geekWorkExpList.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveWorkExpDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="1 >= formContent.geekWorkExpList.length"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeWorkExp(index)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1.25fr 1fr',
|
||||
gap: '10px',
|
||||
width: '100%'
|
||||
}"
|
||||
>
|
||||
<el-form-item
|
||||
label="公司名称"
|
||||
style="margin-bottom: 18px"
|
||||
:prop="`geekWorkExpList_${index}_company`"
|
||||
>
|
||||
<el-input v-model="exp.company" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任职时间" style="margin-bottom: 18px">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="exp.startYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="开始月份"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="exp.endYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="结束月份"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="职务" style="margin-bottom: 18px">
|
||||
<el-input v-model="exp.positionName" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="工作描述" style="margin-bottom: 18px">
|
||||
<el-input
|
||||
v-model="exp.workDescription"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工作业绩">
|
||||
<el-input
|
||||
v-model="exp.performance"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
<div
|
||||
v-if="index !== formContent.geekWorkExpList.length - 1"
|
||||
class="mt20px mb20px h1px"
|
||||
style="background-color: #dcdcdc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="el-form-item__label">
|
||||
项目经历
|
||||
<el-button size="small" :icon="Plus" @click="addProjExp">新增一条</el-button>
|
||||
</div>
|
||||
<div v-for="(proj, index) in formContent.geekProjExpList" :key="index">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
gap: '12px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
height: 'fit-content',
|
||||
marginTop: '10px'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveProjExpUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= formContent.geekProjExpList.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveProjExpDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="1 >= formContent.geekProjExpList.length"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeProjExp(index)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1.25fr',
|
||||
gap: '10px',
|
||||
width: '100%'
|
||||
}"
|
||||
>
|
||||
<el-form-item
|
||||
label="项目名称"
|
||||
style="margin-bottom: 18px"
|
||||
:prop="`geekProjExpList_${index}_name`"
|
||||
>
|
||||
<el-input v-model="proj.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目角色" style="margin-bottom: 18px">
|
||||
<el-input v-model="proj.roleName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目时间" style="margin-bottom: 18px">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="proj.startYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="开始月份"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="proj.endYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="结束月份"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="项目描述" style="margin-bottom: 18px">
|
||||
<el-input
|
||||
v-model="proj.projectDescription"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目业绩">
|
||||
<el-input
|
||||
v-model="proj.performance"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
<div
|
||||
v-if="index !== formContent.geekProjExpList.length - 1"
|
||||
class="mt20px mb20px h1px"
|
||||
style="background-color: #dcdcdc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</main>
|
||||
</div>
|
||||
<footer pt10px pb10px flex flex-justify-center>
|
||||
<div w768px flex flex-justify-between>
|
||||
<div>
|
||||
<!-- <el-button type="text" @click="handleTestAvailability">测试可用性</el-button> -->
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElForm, ElButton, ElAlert, ElMessageBox } from 'element-plus'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { type ResumeContent, resumeContentEnoughDetect } from '../../../../common/utils/resume'
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
|
||||
const getEmptyFormContent = () => {
|
||||
const o: any = {
|
||||
expectJob: '',
|
||||
name: '',
|
||||
userDescription: '',
|
||||
workYearDesc: '',
|
||||
expectSalary: ['', ''],
|
||||
geekWorkExpList: [],
|
||||
geekProjExpList: []
|
||||
}
|
||||
o.geekProjExpList = [getNewProjExpItem()]
|
||||
o.geekWorkExpList = [getNewWorkExpItem()]
|
||||
|
||||
return o as ResumeContent
|
||||
}
|
||||
const formContent = ref<ResumeContent>(getEmptyFormContent())
|
||||
|
||||
const formContentForElForm = computed(() => {
|
||||
const valueMap = {}
|
||||
formContent.value.geekWorkExpList?.forEach((item, i) => {
|
||||
valueMap[`geekWorkExpList_${i}_company`] = item.company
|
||||
})
|
||||
formContent.value.geekProjExpList?.forEach((item, i) => {
|
||||
valueMap[`geekProjExpList_${i}_name`] = item.name
|
||||
})
|
||||
return valueMap
|
||||
})
|
||||
const formRulesForElForm = computed(() => {
|
||||
const valueMap = {}
|
||||
formContent.value.geekWorkExpList.forEach((_, i) => {
|
||||
valueMap[`geekWorkExpList_${i}_company`] = [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入公司名称'
|
||||
},
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator(_, value, cb) {
|
||||
if (!value.trim()) {
|
||||
cb(`请输入公司名称`)
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
formContent.value.geekProjExpList.forEach((_, i) => {
|
||||
valueMap[`geekProjExpList_${i}_name`] = [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入项目名称'
|
||||
},
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator(_, value, cb) {
|
||||
if (!value.trim()) {
|
||||
cb(`请输入项目名称`)
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
return valueMap
|
||||
})
|
||||
const handleCancel = () => {
|
||||
gtagRenderer('cancel_clicked')
|
||||
electron.ipcRenderer.send('close-resume-editor')
|
||||
gtagRenderer('cancel_done')
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
gtagRenderer('submit_clicked')
|
||||
if (
|
||||
!resumeContentEnoughDetect({
|
||||
content: formContent.value
|
||||
})
|
||||
) {
|
||||
try {
|
||||
gtagRenderer('rc_not_enough_dialog_show')
|
||||
await ElMessageBox.confirm(
|
||||
`简历内容可能不够充足(各个部分内容长度相加 <800 字)<br />后续大模型根据简历生成的内容将可能不符合预期(例如相同内容重复生成、生成预期之外的内容)<br /><br />要继续保存吗?`,
|
||||
{
|
||||
cancelButtonText: '不,我再改改',
|
||||
confirmButtonText: '是的,继续保存',
|
||||
dangerouslyUseHTMLString: true
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
electron.ipcRenderer.invoke('save-resume-content', JSON.parse(JSON.stringify(formContent.value)))
|
||||
gtagRenderer('submit_done')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const savedFileContent = await electron.ipcRenderer.invoke('fetch-resume-content')
|
||||
if (!savedFileContent) {
|
||||
return
|
||||
}
|
||||
for (const k of Object.keys(formContent.value)) {
|
||||
formContent.value[k] = savedFileContent[k]
|
||||
}
|
||||
if (!formContent.value.expectSalary) {
|
||||
formContent.value.expectSalary = ['', '']
|
||||
}
|
||||
if (!formContent.value.expectSalary?.[0] || /\D/.test(formContent.value.expectSalary?.[0])) {
|
||||
formContent.value.expectSalary[0] = ''
|
||||
}
|
||||
if (!formContent.value.expectSalary?.[1] || /\D/.test(formContent.value.expectSalary?.[1])) {
|
||||
formContent.value.expectSalary[1] = ''
|
||||
}
|
||||
if (!formContent.value.geekProjExpList?.length) {
|
||||
formContent.value.geekProjExpList = [getNewProjExpItem()]
|
||||
}
|
||||
if (!formContent.value.geekWorkExpList?.length) {
|
||||
formContent.value.geekWorkExpList = [getNewWorkExpItem()]
|
||||
}
|
||||
} catch (err) {
|
||||
formContent.value = getEmptyFormContent()
|
||||
}
|
||||
})
|
||||
|
||||
// function handlePresetClick(selected: (typeof llmPresetList)[number]) {}
|
||||
|
||||
// #region edit work exp list
|
||||
function getNewWorkExpItem() {
|
||||
return {
|
||||
company: '',
|
||||
endYearMon: '',
|
||||
positionName: '',
|
||||
startYearMon: '',
|
||||
performance: '',
|
||||
workDescription: ''
|
||||
}
|
||||
}
|
||||
function addWorkExp() {
|
||||
formContent.value.geekWorkExpList.push(getNewWorkExpItem())
|
||||
gtagRenderer('resume_work_exp_added')
|
||||
}
|
||||
function moveWorkExpUp(index) {
|
||||
;[formContent.value.geekWorkExpList[index], formContent.value.geekWorkExpList[index - 1]] = [
|
||||
formContent.value.geekWorkExpList[index - 1],
|
||||
formContent.value.geekWorkExpList[index]
|
||||
]
|
||||
gtagRenderer('resume_work_exp_moved_up')
|
||||
}
|
||||
|
||||
function moveWorkExpDown(index) {
|
||||
;[formContent.value.geekWorkExpList[index], formContent.value.geekWorkExpList[index + 1]] = [
|
||||
formContent.value.geekWorkExpList[index + 1],
|
||||
formContent.value.geekWorkExpList[index]
|
||||
]
|
||||
gtagRenderer('resume_work_exp_moved_down')
|
||||
}
|
||||
|
||||
function removeWorkExp(index) {
|
||||
formContent.value.geekWorkExpList.splice(index, 1)
|
||||
gtagRenderer('resume_work_exp_removed')
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region edit proj list
|
||||
function getNewProjExpItem() {
|
||||
return {
|
||||
name: '',
|
||||
endYearMon: '',
|
||||
roleName: '',
|
||||
startYearMon: '',
|
||||
performance: '',
|
||||
projectDescription: ''
|
||||
}
|
||||
}
|
||||
function addProjExp() {
|
||||
formContent.value.geekProjExpList.push(getNewProjExpItem())
|
||||
gtagRenderer('resume_proj_exp_added')
|
||||
}
|
||||
function moveProjExpUp(index) {
|
||||
;[formContent.value.geekProjExpList[index], formContent.value.geekProjExpList[index - 1]] = [
|
||||
formContent.value.geekProjExpList[index - 1],
|
||||
formContent.value.geekProjExpList[index]
|
||||
]
|
||||
gtagRenderer('resume_proj_exp_moved_up')
|
||||
}
|
||||
|
||||
function moveProjExpDown(index) {
|
||||
;[formContent.value.geekProjExpList[index], formContent.value.geekProjExpList[index + 1]] = [
|
||||
formContent.value.geekProjExpList[index + 1],
|
||||
formContent.value.geekProjExpList[index]
|
||||
]
|
||||
gtagRenderer('resume_proj_exp_moved_down')
|
||||
}
|
||||
|
||||
function removeProjExp(index) {
|
||||
formContent.value.geekProjExpList.splice(index, 1)
|
||||
gtagRenderer('resume_proj_exp_removed')
|
||||
}
|
||||
// #endregion
|
||||
|
||||
onMounted(() => {
|
||||
gtagRenderer('resume_editor_mounted')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.resume-editor-page {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
.main-wrapper {
|
||||
overflow: auto;
|
||||
main {
|
||||
margin: 0 auto;
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
footer {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.resume-editor-form.el-form {
|
||||
.el-form-item__error--inline {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
import BootstrapSplash from '@renderer/page/BootstrapSplash/index.vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
@@ -13,59 +14,80 @@ const routes: Array<RouteRecordRaw> = [
|
||||
path: '/cookieAssistant',
|
||||
component: () => import('@renderer/page/CookieAssistant/index.vue'),
|
||||
meta: {
|
||||
title: 'Cookie 助手'
|
||||
title: 'Boss 登录助手'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/configuration',
|
||||
component: () => import('@renderer/page/Configuration/index.vue'),
|
||||
redirect: '/configuration/GeekAutoStartChatWithBoss',
|
||||
path: '/llmConfig',
|
||||
component: () => import('@renderer/page/LlmConfig/index.vue'),
|
||||
meta: {
|
||||
title: '大语言模型设置'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/resumeEditor',
|
||||
component: () => import('@renderer/page/ResumeEditor/index.vue'),
|
||||
meta: {
|
||||
title: '简历编辑'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/readNoReplyReminderLlmMock',
|
||||
component: () => import('@renderer/page/ReadNoReplyReminderLlmMock/index.vue'),
|
||||
meta: {
|
||||
title: '已读不回提醒器 大语言模型测试'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/main-layout',
|
||||
component: () => import('@renderer/page/MainLayout/index.vue'),
|
||||
redirect: '/main-layout/GeekAutoStartChatWithBoss',
|
||||
children: [
|
||||
{
|
||||
path: 'GeekAutoStartChatWithBoss',
|
||||
component: () => import('@renderer/page/Configuration/GeekAutoStartChatWithBoss.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss.vue'),
|
||||
meta: {
|
||||
title: 'BOSS炸弹'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ReadNoReplyReminder',
|
||||
component: () => import('@renderer/page/Configuration/ReadNoReplyReminder.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/ReadNoReplyReminder.vue'),
|
||||
meta: {
|
||||
title: '已读不回提醒器'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'StartChatRecord',
|
||||
component: () => import('@renderer/page/Configuration/StartChatRecord.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/StartChatRecord.vue'),
|
||||
meta: {
|
||||
title: '开聊记录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'MarkAsNotSuitRecord',
|
||||
component: () => import('@renderer/page/Configuration/MarkAsNotSuitRecord.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/MarkAsNotSuitRecord.vue'),
|
||||
meta: {
|
||||
title: '标记不合适记录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'JobLibrary',
|
||||
component: () => import('@renderer/page/Configuration/JobLibrary.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/JobLibrary.vue'),
|
||||
meta: {
|
||||
title: '职位库'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'BossLibrary',
|
||||
component: () => import('@renderer/page/Configuration/BossLibrary.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/BossLibrary.vue'),
|
||||
meta: {
|
||||
title: 'Boss库'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'CompanyLibrary',
|
||||
component: () => import('@renderer/page/Configuration/CompanyLibrary.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/CompanyLibrary.vue'),
|
||||
meta: {
|
||||
title: '公司库'
|
||||
}
|
||||
@@ -114,7 +136,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import('@renderer/page/BootstrapSplash/page/DownloadingDependencies.vue'),
|
||||
meta: {
|
||||
title: '正在下载核心组件'
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -125,12 +147,20 @@ const router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
router.afterEach((to, from) => {
|
||||
if (to.meta?.title) {
|
||||
document.title = `${to.meta.title} - GeekGeekRun`
|
||||
document.title = `${to.meta.title} - GeekGeekRun 牛人快跑`
|
||||
} else {
|
||||
document.title = `GeekGeekRun`
|
||||
document.title = `GeekGeekRun 牛人快跑`
|
||||
}
|
||||
gtagRenderer('router_path_changed', {
|
||||
from_path: from.fullPath,
|
||||
to_path: to.fullPath
|
||||
})
|
||||
gtagRenderer('page_view', {
|
||||
page_location: location.href,
|
||||
page_title: document.title
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
20
packages/ui/src/renderer/src/store/index.ts
Normal file
20
packages/ui/src/renderer/src/store/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { NewReleaseInfo } from '../../../common/types/update'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useUpdateStore = defineStore('update', () => {
|
||||
const availableNewRelease = ref<NewReleaseInfo | null>(null)
|
||||
|
||||
async function checkUpdate() {
|
||||
let result: NewReleaseInfo | null = null
|
||||
try {
|
||||
result = (await electron.ipcRenderer.invoke('check-update')) as NewReleaseInfo | null
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
availableNewRelease.value = result
|
||||
}
|
||||
checkUpdate()
|
||||
setInterval(checkUpdate, 30 * 30 * 1000)
|
||||
return { availableNewRelease }
|
||||
})
|
||||
15
packages/ui/src/renderer/src/utils/gtag.ts
Normal file
15
packages/ui/src/renderer/src/utils/gtag.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function gtagRenderer(name, params: any = null) {
|
||||
try {
|
||||
electron.ipcRenderer.send('gtag', {
|
||||
name,
|
||||
params: {
|
||||
...(params ?? {}),
|
||||
screen_w: window.screen?.width ?? null,
|
||||
screen_h: window.screen?.height ?? null,
|
||||
screen_dpr: window.devicePixelRatio
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('gtag error', err)
|
||||
}
|
||||
}
|
||||
3
packages/ui/src/renderer/src/utils/mitt.ts
Normal file
3
packages/ui/src/renderer/src/utils/mitt.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import mitt from 'mitt'
|
||||
const mittBus = mitt()
|
||||
export default mittBus
|
||||
26
packages/utils/gpt-request.mjs
Normal file
26
packages/utils/gpt-request.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
import OpenAI from "openai";
|
||||
|
||||
export async function completes(
|
||||
{
|
||||
baseURL,
|
||||
apiKey,
|
||||
model
|
||||
},
|
||||
messages
|
||||
) {
|
||||
const openai = new OpenAI({
|
||||
baseURL,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
messages,
|
||||
model,
|
||||
frequency_penalty: 0,
|
||||
max_tokens: 100,
|
||||
temperature: 0.1
|
||||
});
|
||||
|
||||
console.log(completion.choices[0].message.content);
|
||||
return completion;
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "utils",
|
||||
"scripts": {
|
||||
},
|
||||
"scripts": {},
|
||||
"author": "geekgeekrun",
|
||||
"license": "ISC",
|
||||
"dependencies": {}
|
||||
"dependencies": {
|
||||
"openai": "^4.91.1"
|
||||
}
|
||||
}
|
||||
|
||||
195
pnpm-lock.yaml
generated
195
pnpm-lock.yaml
generated
@@ -132,15 +132,24 @@ importers:
|
||||
minimist:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
mitt:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
node-machine-id:
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12
|
||||
pinia:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(typescript@5.3.3)(vue@3.4.15)
|
||||
puppeteer:
|
||||
specifier: 20.1.0
|
||||
version: 20.1.0(typescript@5.3.3)
|
||||
puppeteer-extra-plugin-stealth:
|
||||
specifier: 2.11.2
|
||||
version: 2.11.2(puppeteer-extra@3.3.6)
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
devDependencies:
|
||||
'@electron-toolkit/eslint-config':
|
||||
specifier: ^1.0.2
|
||||
@@ -226,6 +235,9 @@ importers:
|
||||
sass:
|
||||
specifier: ^1.70.0
|
||||
version: 1.70.0
|
||||
spark-md5:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
terser:
|
||||
specifier: ^5.37.0
|
||||
version: 5.37.0
|
||||
@@ -257,7 +269,11 @@ importers:
|
||||
specifier: ^1.8.27
|
||||
version: 1.8.27(typescript@5.3.3)
|
||||
|
||||
packages/utils: {}
|
||||
packages/utils:
|
||||
dependencies:
|
||||
openai:
|
||||
specifier: ^4.91.1
|
||||
version: 4.91.1
|
||||
|
||||
packages:
|
||||
|
||||
@@ -466,7 +482,6 @@ packages:
|
||||
/@babel/helper-string-parser@7.23.4:
|
||||
resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-validator-identifier@7.22.20:
|
||||
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
|
||||
@@ -502,7 +517,6 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.23.9
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.9):
|
||||
resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==}
|
||||
@@ -614,7 +628,6 @@ packages:
|
||||
'@babel/helper-string-parser': 7.23.4
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@cspotcode/source-map-support@0.8.1:
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
@@ -1679,6 +1692,13 @@ packages:
|
||||
/@types/ms@0.7.34:
|
||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||
|
||||
/@types/node-fetch@2.6.12:
|
||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||
dependencies:
|
||||
'@types/node': 18.19.15
|
||||
form-data: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@types/node@18.19.15:
|
||||
resolution: {integrity: sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==}
|
||||
dependencies:
|
||||
@@ -2113,7 +2133,6 @@ packages:
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-core@3.4.18:
|
||||
resolution: {integrity: sha512-F7YK8lMK0iv6b9/Gdk15A67wM0KKZvxDxed0RR60C1z9tIJTKta+urs4j0RTN5XqHISzI3etN3mX0uHhjmoqjQ==}
|
||||
@@ -2130,7 +2149,6 @@ packages:
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.4.15
|
||||
'@vue/shared': 3.4.15
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-dom@3.4.18:
|
||||
resolution: {integrity: sha512-24Eb8lcMfInefvQ6YlEVS18w5Q66f4+uXWVA+yb7praKbyjHRNuKVWGuinfSSjM0ZIiPi++QWukhkgznBaqpEA==}
|
||||
@@ -2151,19 +2169,41 @@ packages:
|
||||
magic-string: 0.30.7
|
||||
postcss: 8.4.35
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-ssr@3.4.15:
|
||||
resolution: {integrity: sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==}
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.4.15
|
||||
'@vue/shared': 3.4.15
|
||||
dev: true
|
||||
|
||||
/@vue/devtools-api@6.5.1:
|
||||
resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==}
|
||||
dev: true
|
||||
|
||||
/@vue/devtools-api@7.7.5:
|
||||
resolution: {integrity: sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==}
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.5
|
||||
dev: false
|
||||
|
||||
/@vue/devtools-kit@7.7.5:
|
||||
resolution: {integrity: sha512-S9VAVJYVAe4RPx2JZb9ZTEi0lqTySz2CBeF0wHT5D3dkTLnT9yMMGegKNl4b2EIELwLSkcI9bl2qp0/jW+upqA==}
|
||||
dependencies:
|
||||
'@vue/devtools-shared': 7.7.5
|
||||
birpc: 2.3.0
|
||||
hookable: 5.5.3
|
||||
mitt: 3.0.1
|
||||
perfect-debounce: 1.0.0
|
||||
speakingurl: 14.0.1
|
||||
superjson: 2.2.2
|
||||
dev: false
|
||||
|
||||
/@vue/devtools-shared@7.7.5:
|
||||
resolution: {integrity: sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==}
|
||||
dependencies:
|
||||
rfdc: 1.4.1
|
||||
dev: false
|
||||
|
||||
/@vue/eslint-config-prettier@9.0.0(eslint@8.56.0)(prettier@3.2.4):
|
||||
resolution: {integrity: sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==}
|
||||
peerDependencies:
|
||||
@@ -2223,14 +2263,12 @@ packages:
|
||||
resolution: {integrity: sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==}
|
||||
dependencies:
|
||||
'@vue/shared': 3.4.15
|
||||
dev: true
|
||||
|
||||
/@vue/runtime-core@3.4.15:
|
||||
resolution: {integrity: sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==}
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.4.15
|
||||
'@vue/shared': 3.4.15
|
||||
dev: true
|
||||
|
||||
/@vue/runtime-dom@3.4.15:
|
||||
resolution: {integrity: sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==}
|
||||
@@ -2238,7 +2276,6 @@ packages:
|
||||
'@vue/runtime-core': 3.4.15
|
||||
'@vue/shared': 3.4.15
|
||||
csstype: 3.1.3
|
||||
dev: true
|
||||
|
||||
/@vue/server-renderer@3.4.15(vue@3.4.15):
|
||||
resolution: {integrity: sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==}
|
||||
@@ -2248,11 +2285,9 @@ packages:
|
||||
'@vue/compiler-ssr': 3.4.15
|
||||
'@vue/shared': 3.4.15
|
||||
vue: 3.4.15(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/@vue/shared@3.4.15:
|
||||
resolution: {integrity: sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==}
|
||||
dev: true
|
||||
|
||||
/@vue/shared@3.4.18:
|
||||
resolution: {integrity: sha512-CxouGFxxaW5r1WbrSmWwck3No58rApXgRSBxrqgnY1K+jk20F6DrXJkHdH9n4HVT+/B6G2CAn213Uq3npWiy8Q==}
|
||||
@@ -2302,6 +2337,13 @@ packages:
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
|
||||
/abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
dev: false
|
||||
|
||||
/acorn-jsx@5.3.2(acorn@8.11.3):
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -2349,7 +2391,6 @@ packages:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/aggregate-error@3.1.0:
|
||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||
@@ -2546,7 +2587,6 @@ packages:
|
||||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: true
|
||||
|
||||
/at-least-node@1.0.0:
|
||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||
@@ -2574,6 +2614,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/birpc@2.3.0:
|
||||
resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==}
|
||||
dev: false
|
||||
|
||||
/bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
dependencies:
|
||||
@@ -2904,7 +2948,6 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: true
|
||||
|
||||
/commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
@@ -2947,6 +2990,13 @@ packages:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
dev: true
|
||||
|
||||
/copy-anything@3.0.5:
|
||||
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
||||
engines: {node: '>=12.13'}
|
||||
dependencies:
|
||||
is-what: 4.1.16
|
||||
dev: false
|
||||
|
||||
/core-util-is@1.0.2:
|
||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
||||
requiresBuild: true
|
||||
@@ -3015,7 +3065,6 @@ packages:
|
||||
|
||||
/csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
dev: true
|
||||
|
||||
/data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
@@ -3108,7 +3157,6 @@ packages:
|
||||
/delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/delegates@1.0.0:
|
||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||
@@ -3360,7 +3408,6 @@ packages:
|
||||
/entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
dev: true
|
||||
|
||||
/env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
@@ -3594,7 +3641,6 @@ packages:
|
||||
|
||||
/estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
dev: true
|
||||
|
||||
/estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
@@ -3607,6 +3653,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/execa@5.1.1:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3776,6 +3827,10 @@ packages:
|
||||
signal-exit: 4.1.0
|
||||
dev: true
|
||||
|
||||
/form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
dev: false
|
||||
|
||||
/form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3783,7 +3838,14 @@ packages:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/formdata-node@4.4.1:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
dev: false
|
||||
|
||||
/formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
@@ -4081,6 +4143,10 @@ packages:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
dev: false
|
||||
|
||||
/hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
dev: false
|
||||
|
||||
/hosted-git-info@4.1.0:
|
||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4162,7 +4228,6 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/iconv-corefoundation@1.1.7:
|
||||
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
||||
@@ -4318,6 +4383,11 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-what@4.1.16:
|
||||
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
||||
engines: {node: '>=12.13'}
|
||||
dev: false
|
||||
|
||||
/isbinaryfile@4.0.10:
|
||||
resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@@ -4559,7 +4629,6 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
@@ -4643,14 +4712,12 @@ packages:
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: true
|
||||
|
||||
/mime@2.6.0:
|
||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||
@@ -4768,6 +4835,10 @@ packages:
|
||||
resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==}
|
||||
dev: false
|
||||
|
||||
/mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
dev: false
|
||||
|
||||
/mixin-object@2.0.1:
|
||||
resolution: {integrity: sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4822,7 +4893,6 @@ packages:
|
||||
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
@@ -5002,6 +5072,29 @@ packages:
|
||||
mimic-fn: 2.1.0
|
||||
dev: true
|
||||
|
||||
/openai@4.91.1:
|
||||
resolution: {integrity: sha512-DbjrR0hIMQFbxz8+3qBsfPJnh3+I/skPgoSlT7f9eiZuhGBUissPQULNgx6gHNkLoZ3uS0uYS6eXPUdtg4nHzw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.19.15
|
||||
'@types/node-fetch': 2.6.12
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.5.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.6.7
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/optionator@0.9.3:
|
||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5144,17 +5237,29 @@ packages:
|
||||
|
||||
/perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
dev: true
|
||||
|
||||
/picocolors@1.0.0:
|
||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||
dev: true
|
||||
|
||||
/picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
dev: true
|
||||
|
||||
/pinia@3.0.2(typescript@5.3.3)(vue@3.4.15):
|
||||
resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.4.4'
|
||||
vue: ^2.7.0 || ^3.5.11
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.5
|
||||
typescript: 5.3.3
|
||||
vue: 3.4.15(typescript@5.3.3)
|
||||
dev: false
|
||||
|
||||
/pkg-types@1.0.3:
|
||||
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
|
||||
dependencies:
|
||||
@@ -5187,7 +5292,6 @@ packages:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
@@ -5497,6 +5601,10 @@ packages:
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
dev: false
|
||||
|
||||
/rimraf@3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
hasBin: true
|
||||
@@ -5711,7 +5819,6 @@ packages:
|
||||
/source-map-js@1.0.2:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/source-map-support@0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
@@ -5726,6 +5833,15 @@ packages:
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
/spark-md5@3.0.2:
|
||||
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||
dev: true
|
||||
|
||||
/speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/sprintf-js@1.1.3:
|
||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||
requiresBuild: true
|
||||
@@ -5827,6 +5943,13 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/superjson@2.2.2:
|
||||
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
|
||||
engines: {node: '>=16'}
|
||||
dependencies:
|
||||
copy-anything: 3.0.5
|
||||
dev: false
|
||||
|
||||
/supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -5969,7 +6092,6 @@ packages:
|
||||
/to-fast-properties@2.0.0:
|
||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
@@ -6360,6 +6482,11 @@ packages:
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
/uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
@@ -6492,13 +6619,17 @@ packages:
|
||||
'@vue/server-renderer': 3.4.15(vue@3.4.15)
|
||||
'@vue/shared': 3.4.15
|
||||
typescript: 5.3.3
|
||||
dev: true
|
||||
|
||||
/web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
dev: false
|
||||
|
||||
/web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
|
||||
Reference in New Issue
Block a user