mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-07 08:27:13 +08:00
Merge branch 'feature/auto-reminder-gpt' into feature/ui
This commit is contained in:
@@ -9,6 +9,10 @@
|
|||||||
"expectJobRegExpStr": "",
|
"expectJobRegExpStr": "",
|
||||||
"autoReminder": {
|
"autoReminder": {
|
||||||
"throttleIntervalMinutes": 10,
|
"throttleIntervalMinutes": 10,
|
||||||
"rechatLimitDay": 21
|
"rechatLimitDay": 21,
|
||||||
|
"geminiApiKey": "",
|
||||||
|
"rechatContentSource": 1,
|
||||||
|
"recentMessageQuantityForLlm": 8,
|
||||||
|
"rechatLlmFallback": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[{
|
||||||
|
"providerCompleteApiUrl": "",
|
||||||
|
"providerApiSecret": "",
|
||||||
|
"model": "",
|
||||||
|
"enabled": true,
|
||||||
|
"serveWeight": 100,
|
||||||
|
"_extra": {}
|
||||||
|
}]
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
[
|
[
|
||||||
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技",
|
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
|
||||||
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","京东","沃东天骏","达达","百度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","理想","蔚来","顺丰","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","蚂蚁","飞猪","高德","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","得物","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","首汽租车","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技"
|
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","京东","沃东天骏","达达","达冠","百度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","理想","蔚来","顺丰","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","得物","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","首汽租车","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手"
|
||||||
]
|
]
|
||||||
@@ -79,6 +79,21 @@ const targetCompanyList = readConfigFile('target-company-list.json').filter(it =
|
|||||||
|
|
||||||
const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter
|
const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter
|
||||||
const expectJobRegExpStr = readConfigFile('boss.json').expectJobRegExpStr
|
const expectJobRegExpStr = readConfigFile('boss.json').expectJobRegExpStr
|
||||||
|
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 localStoragePageUrl = `https://www.zhipin.com/desktop/`
|
||||||
const recommendJobPageUrl = `https://www.zhipin.com/web/geek/job-recommend`
|
const recommendJobPageUrl = `https://www.zhipin.com/web/geek/job-recommend`
|
||||||
@@ -191,22 +206,32 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function testIfJobTitleOrDescriptionSuit (jobInfo, regExpStr) {
|
export function testIfJobTitleOrDescriptionSuit (jobInfo) {
|
||||||
if (!regExpStr) {
|
let isJobNameSuit = true
|
||||||
return true
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const regExp = new RegExp(regExpStr, 'i')
|
if (expectJobNameRegExpStr.trim()) {
|
||||||
if (
|
const regExp = new RegExp(expectJobNameRegExpStr, 'i')
|
||||||
!regExp.test(jobInfo.jobName)
|
isJobNameSuit = regExp.test(jobInfo.jobName)
|
||||||
&& !regExp.test(jobInfo.positionName)
|
|
||||||
&& !regExp.test(jobInfo.postDescription)
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
} catch {
|
} 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) {
|
async function setFilterCondition (selectedFilters) {
|
||||||
@@ -239,7 +264,7 @@ async function setFilterCondition (selectedFilters) {
|
|||||||
const placeholderText = placeholderTexts[i]
|
const placeholderText = placeholderTexts[i]
|
||||||
const filterDropdownProxy = await (async () => {
|
const filterDropdownProxy = await (async () => {
|
||||||
const jsHandle = (await page.evaluateHandle((placeholderText) => {
|
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)
|
const dropdownEntry = filterBar.__vue__.$children.find(it => it.placeholder === placeholderText)
|
||||||
return dropdownEntry.$el
|
return dropdownEntry.$el
|
||||||
}, placeholderText)).asElement();
|
}, placeholderText)).asElement();
|
||||||
@@ -264,18 +289,18 @@ async function setFilterCondition (selectedFilters) {
|
|||||||
const optionKaPrefix = optionKaPrefixes[i]
|
const optionKaPrefix = optionKaPrefixes[i]
|
||||||
if (!currentFilterConditions.length) {
|
if (!currentFilterConditions.length) {
|
||||||
if (placeholderText === '公司行业') {
|
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) {
|
for (const it of activeOptionElAtCurrentFilterProxyList) {
|
||||||
await it.click()
|
await it.click()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// select 不限 immediately
|
// 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()
|
await buxianOptionElProxy.click()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//#region uncheck options perviously checked but not existed in current filter.
|
//#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(
|
const activeOptionValues = (await Promise.all(
|
||||||
activeOptionElAtCurrentFilterProxyList.map(elProxy => {
|
activeOptionElAtCurrentFilterProxyList.map(elProxy => {
|
||||||
return elProxy.evaluate((el) => {
|
return elProxy.evaluate((el) => {
|
||||||
@@ -314,7 +339,7 @@ async function setFilterCondition (selectedFilters) {
|
|||||||
optionValue = conditionToCheck[j]
|
optionValue = conditionToCheck[j]
|
||||||
}
|
}
|
||||||
await sleepWithRandomDelay(500)
|
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) {
|
if (!optionElProxy) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -384,7 +409,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
|
let currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||||||
afterPageLoad: while (true) {
|
afterPageLoad: while (true) {
|
||||||
let expectJobList
|
let expectJobList
|
||||||
@@ -397,20 +422,21 @@ async function toRecommendPage (hooks) {
|
|||||||
await sleepWithRandomDelay(2500)
|
await sleepWithRandomDelay(2500)
|
||||||
|
|
||||||
await Promise.all([
|
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')
|
page.waitForSelector('.job-list-container .rec-job-list')
|
||||||
])
|
])
|
||||||
|
await page.click(`.c-expect-select .expect-list .expect-item`)
|
||||||
const currentActiveJobIndex = await page.evaluate(`
|
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) {
|
if (currentActiveJobIndex === currentExceptJobIndex) {
|
||||||
// first navigation and can immediately start chat (recommend job)
|
// first navigation and can immediately start chat (recommend job)
|
||||||
} else {
|
} else {
|
||||||
// not first navigation and should choose a job (except job)
|
// not first navigation and should choose a job (except job)
|
||||||
// click first expect 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 expectJobTabHandlers[currentExceptJobIndex].click()
|
||||||
await page.waitForResponse(
|
await page.waitForResponse(
|
||||||
response => {
|
response => {
|
||||||
@@ -461,7 +487,7 @@ async function toRecommendPage (hooks) {
|
|||||||
|
|
||||||
// job list
|
// job list
|
||||||
const recommendJobListElProxy = await page.$('.job-list-container .rec-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 hasReachLastPage = false
|
||||||
let targetJobIndex = -1
|
let targetJobIndex = -1
|
||||||
let targetJobData
|
let targetJobData
|
||||||
@@ -494,10 +520,10 @@ async function toRecommendPage (hooks) {
|
|||||||
) {
|
) {
|
||||||
scrolledHeight += increase
|
scrolledHeight += increase
|
||||||
await page.mouse.wheel({deltaY: increase});
|
await page.mouse.wheel({deltaY: increase});
|
||||||
await sleep(1)
|
await sleep(100)
|
||||||
await requestNextPagePromiseWithResolver?.promise
|
await requestNextPagePromiseWithResolver?.promise
|
||||||
hasReachLastPage = await page.evaluate(`
|
hasReachLastPage = await page.evaluate(`
|
||||||
!(document.querySelector('.job-recommend-main')?.__vue__?.hasMore)
|
!(document.querySelector('.page-jobs-main')?.__vue__?.hasMore)
|
||||||
`)
|
`)
|
||||||
if (hasReachLastPage) {
|
if (hasReachLastPage) {
|
||||||
console.log(`Arrive the terminal of the job list.`)
|
console.log(`Arrive the terminal of the job list.`)
|
||||||
@@ -505,16 +531,16 @@ async function toRecommendPage (hooks) {
|
|||||||
}
|
}
|
||||||
requestNextPagePromiseWithResolver = null
|
requestNextPagePromiseWithResolver = null
|
||||||
|
|
||||||
await sleep(3000)
|
await sleep(5000)
|
||||||
jobListData = await page.evaluate(
|
jobListData = await page.evaluate(
|
||||||
`
|
`
|
||||||
document.querySelector('.job-recommend-main')?.__vue__?.jobList
|
document.querySelector('.page-jobs-main')?.__vue__?.jobList
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
tempTargetJobIndexToCheckDetail = jobListData.findIndex(it =>
|
tempTargetJobIndexToCheckDetail = jobListData.findIndex(it =>
|
||||||
!blockBossNotNewChat.has(it.encryptBossId) &&
|
!blockBossNotNewChat.has(it.encryptBossId) &&
|
||||||
!blockBossNotActive.has(it.encryptBossId) &&
|
!blockBossNotActive.has(it.encryptBossId) &&
|
||||||
[...expectCompanySet].find(name => it.brandName.includes(name)) &&
|
[...expectCompanySet].find(name => it.brandName?.toLowerCase?.()?.includes(name.toLowerCase())) &&
|
||||||
!blockJobNotSuit.has(it.encryptJobId)
|
!blockJobNotSuit.has(it.encryptJobId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -543,6 +569,10 @@ async function toRecommendPage (hooks) {
|
|||||||
const recommendJobItemList = await recommendJobListElProxy.$$('ul.rec-job-list li.job-card-box')
|
const recommendJobItemList = await recommendJobListElProxy.$$('ul.rec-job-list li.job-card-box')
|
||||||
const targetJobElProxy = recommendJobItemList[tempTargetJobIndexToCheckDetail]
|
const targetJobElProxy = recommendJobItemList[tempTargetJobIndexToCheckDetail]
|
||||||
// click that element
|
// click that element
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.documentElement.scrollTop = 0
|
||||||
|
})
|
||||||
|
await sleep(500)
|
||||||
await targetJobElProxy.click()
|
await targetJobElProxy.click()
|
||||||
await page.waitForResponse(
|
await page.waitForResponse(
|
||||||
response => {
|
response => {
|
||||||
@@ -594,7 +624,7 @@ async function toRecommendPage (hooks) {
|
|||||||
continue continueFind
|
continue continueFind
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo, expectJobRegExpStr)
|
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo)
|
||||||
) {
|
) {
|
||||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||||
try {
|
try {
|
||||||
@@ -612,7 +642,6 @@ async function toRecommendPage (hooks) {
|
|||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
debugger
|
|
||||||
continue continueFind
|
continue continueFind
|
||||||
}
|
}
|
||||||
const startChatButtonInnerHTML = await page.evaluate('document.querySelector(".job-detail-box .op-btn.op-btn-chat")?.innerHTML.trim()')
|
const startChatButtonInnerHTML = await page.evaluate('document.querySelector(".job-detail-box .op-btn.op-btn-chat")?.innerHTML.trim()')
|
||||||
@@ -646,6 +675,10 @@ async function toRecommendPage (hooks) {
|
|||||||
|
|
||||||
await hooks.newChatWillStartup?.promise(targetJobData)
|
await hooks.newChatWillStartup?.promise(targetJobData)
|
||||||
const startChatButtonProxy = await page.$('.job-detail-box .op-btn.op-btn-chat')
|
const startChatButtonProxy = await page.$('.job-detail-box .op-btn.op-btn-chat')
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.documentElement.scrollTop = 0
|
||||||
|
})
|
||||||
|
await sleep(500)
|
||||||
//#region click the chat button
|
//#region click the chat button
|
||||||
await startChatButtonProxy.click()
|
await startChatButtonProxy.click()
|
||||||
|
|
||||||
@@ -714,9 +747,11 @@ async function toRecommendPage (hooks) {
|
|||||||
}
|
}
|
||||||
// for of reach terminal
|
// for of reach terminal
|
||||||
if (
|
if (
|
||||||
currentExceptJobIndex + 1 > expectJobList.length
|
currentExceptJobIndex + 1 >= expectJobList.length
|
||||||
) {
|
) {
|
||||||
hooks.noPositionFoundForCurrentJob?.call()
|
hooks.noPositionFoundForCurrentJob?.call()
|
||||||
|
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
||||||
|
await sleep((20 + 30 * Math.random()) * 1000)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.reload(),
|
page.reload(),
|
||||||
page.waitForNavigation()
|
page.waitForNavigation()
|
||||||
@@ -724,8 +759,7 @@ async function toRecommendPage (hooks) {
|
|||||||
currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||||||
} else {
|
} else {
|
||||||
hooks.noPositionFoundForCurrentJob?.call()
|
hooks.noPositionFoundForCurrentJob?.call()
|
||||||
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
await sleep((10 + 15 * Math.random()) * 1000)
|
||||||
|
|
||||||
currentExceptJobIndex += 1
|
currentExceptJobIndex += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ import os from 'node:os'
|
|||||||
import defaultDingtalkConf from './default-config-file/dingtalk.json' assert {type: 'json'}
|
import defaultDingtalkConf from './default-config-file/dingtalk.json' assert {type: 'json'}
|
||||||
import defaultBossConf from './default-config-file/boss.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 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 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 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' }
|
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 = {
|
const defaultConfigFileContentMap = {
|
||||||
'boss.json': JSON.stringify(defaultBossConf),
|
'boss.json': JSON.stringify(defaultBossConf),
|
||||||
'dingtalk.json': JSON.stringify(defaultDingtalkConf),
|
'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')
|
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||||
@@ -69,8 +71,12 @@ export const readConfigFile = (fileName) => {
|
|||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
||||||
ensureConfigFileExist()
|
if (defaultConfigFileContentMap[fileName]) {
|
||||||
o = JSON.parse(defaultConfigFileContentMap[fileName])
|
ensureConfigFileExist()
|
||||||
|
o = JSON.parse(defaultConfigFileContentMap[fileName])
|
||||||
|
} else {
|
||||||
|
o = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return o
|
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)
|
const joinedPath = path.join(storageFilePath, fileName)
|
||||||
|
|
||||||
if (!fs.existsSync(
|
if (!fs.existsSync(
|
||||||
@@ -123,21 +130,36 @@ export const readStorageFile = (fileName) => {
|
|||||||
|
|
||||||
let o
|
let o
|
||||||
try {
|
try {
|
||||||
o = JSON.parse(
|
const content = fs.readFileSync(joinedPath)
|
||||||
fs.readFileSync(joinedPath)
|
if (isJson) {
|
||||||
)
|
o = JSON.parse(content)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
o = content.toString()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
||||||
ensureStorageFileExist()
|
ensureStorageFileExist()
|
||||||
o = JSON.parse(defaultStorageFileContentMap[fileName])
|
if (isJson) {
|
||||||
|
o = JSON.parse(defaultStorageFileContentMap[fileName] ?? 'null')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
o = defaultStorageFileContentMap[fileName] ?? null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return o
|
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 filePath = path.join(storageFilePath, fileName)
|
||||||
const fileContent = JSON.stringify(content)
|
let fileContent
|
||||||
|
if (isJson) {
|
||||||
|
fileContent = JSON.stringify(content)
|
||||||
|
} else {
|
||||||
|
fileContent = content
|
||||||
|
}
|
||||||
return fsPromise.writeFile(
|
return fsPromise.writeFile(
|
||||||
filePath,
|
filePath,
|
||||||
fileContent
|
fileContent
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -6,68 +6,28 @@ import {
|
|||||||
sleep,
|
sleep,
|
||||||
sleepWithRandomDelay
|
sleepWithRandomDelay
|
||||||
} from '@geekgeekrun/utils/sleep.mjs'
|
} from '@geekgeekrun/utils/sleep.mjs'
|
||||||
import extractZip from 'extract-zip'
|
|
||||||
import { blockNavigation } from '@geekgeekrun/utils/puppeteer/block-navigation.mjs'
|
import { blockNavigation } from '@geekgeekrun/utils/puppeteer/block-navigation.mjs'
|
||||||
import {
|
import {
|
||||||
writeStorageFile
|
writeStorageFile
|
||||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||||
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import os from 'node:os'
|
|
||||||
import path from 'node:path';
|
|
||||||
import JSON5 from 'json5'
|
import JSON5 from 'json5'
|
||||||
import url from 'url';
|
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'
|
import { EventEmitter } from 'node:events'
|
||||||
|
|
||||||
export const loginEventBus = new EventEmitter()
|
export const loginEventBus = new EventEmitter()
|
||||||
|
|
||||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
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() {
|
export async function main() {
|
||||||
if (!fs.existsSync(
|
await ensureEditThisCookie()
|
||||||
path.join(editThisCookieExtensionPath, 'manifest.json')
|
|
||||||
)) {
|
|
||||||
await extractZip(
|
|
||||||
await getEditThisCookieZipPath(),
|
|
||||||
{
|
|
||||||
dir: extensionDir
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { puppeteer } = await initPuppeteer()
|
const { puppeteer } = await initPuppeteer()
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
headless: false,
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ export class ChatMessageRecord {
|
|||||||
@Column({
|
@Column({
|
||||||
nullable: true
|
nullable: true
|
||||||
})
|
})
|
||||||
style?: 'sent' | 'receive';
|
style?: 'sent' | 'received';
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { CompanyInfoChangeLog } from "./entity/CompanyInfoChangeLog";
|
|||||||
import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
|
import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
|
||||||
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
|
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
|
||||||
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
|
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
|
||||||
|
import { LlmModelUsageRecord } from "./entity/LlmModelUsageRecord";
|
||||||
|
|
||||||
function getBossInfoIfIsEqual (savedOne, currentOne) {
|
function getBossInfoIfIsEqual (savedOne, currentOne) {
|
||||||
if (savedOne === currentOne) {
|
if (savedOne === currentOne) {
|
||||||
@@ -307,3 +308,21 @@ export async function saveChatMessageRecord(
|
|||||||
//#endregion
|
//#endregion
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { VJobLibrary } from "./entity/VJobLibrary";
|
|||||||
import { VCompanyLibrary } from "./entity/VCompanyLibrary"
|
import { VCompanyLibrary } from "./entity/VCompanyLibrary"
|
||||||
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
|
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
|
||||||
import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
||||||
|
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
||||||
|
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord } from "./handlers";
|
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord } from "./handlers";
|
||||||
@@ -53,10 +54,11 @@ export function initDb(dbFilePath) {
|
|||||||
MarkAsNotSuitLog,
|
MarkAsNotSuitLog,
|
||||||
VMarkAsNotSuitLog,
|
VMarkAsNotSuitLog,
|
||||||
ChatMessageRecord,
|
ChatMessageRecord,
|
||||||
|
LlmModelUsageRecord,
|
||||||
],
|
],
|
||||||
migrations: [
|
migrations: [
|
||||||
UpdateChatStartupLogTable1729182577167,
|
UpdateChatStartupLogTable1729182577167,
|
||||||
UpdateBossInfoTable1732032381304
|
UpdateBossInfoTable1732032381304,
|
||||||
],
|
],
|
||||||
migrationsRun: true
|
migrationsRun: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,31 +5,28 @@ const viewNames = [
|
|||||||
"v_chat_startup_log",
|
"v_chat_startup_log",
|
||||||
"v_company_library",
|
"v_company_library",
|
||||||
"v_job_library",
|
"v_job_library",
|
||||||
"v_mark_as_not_suit_log"
|
"v_mark_as_not_suit_log",
|
||||||
];
|
];
|
||||||
|
|
||||||
export class UpdateBossInfoTable1732032381304 implements MigrationInterface {
|
export class UpdateBossInfoTable1732032381304 implements MigrationInterface {
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
for (const viewName of viewNames) {
|
for (const viewName of viewNames) {
|
||||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (await queryRunner.hasTable("boss_info")) {
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) {
|
||||||
|
await queryRunner.changeColumn(
|
||||||
|
"boss_info",
|
||||||
|
"encryptCompanyId",
|
||||||
|
new TableColumn({
|
||||||
|
name: "encryptCompanyId",
|
||||||
|
type: "varchar",
|
||||||
|
isNullable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,8 @@
|
|||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"node-machine-id": "^1.1.12",
|
"node-machine-id": "^1.1.12",
|
||||||
"puppeteer": "20.1.0",
|
"puppeteer": "20.1.0",
|
||||||
"puppeteer-extra-plugin-stealth": "2.11.2"
|
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.70.0",
|
||||||
|
"spark-md5": "^3.0.2",
|
||||||
"terser": "^5.37.0",
|
"terser": "^5.37.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"unocss": "^0.58.5",
|
"unocss": "^0.58.5",
|
||||||
|
|||||||
@@ -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_DAEMON_PROCESS_SUICIDE = 86,
|
||||||
AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87,
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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]?.positionName?.trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resumeContentEnoughDetect(resumeItem: { content: ResumeContent }) {
|
||||||
|
return resumeItem?.content && formatResumeJsonToMarkdown(resumeItem)?.length > 800
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import { initPuppeteer } from '@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
|
import { initPuppeteer } from '@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
|
||||||
import extractZip from 'extract-zip'
|
|
||||||
import {
|
import {
|
||||||
readStorageFile,
|
readStorageFile,
|
||||||
writeStorageFile
|
writeStorageFile
|
||||||
@@ -17,10 +16,6 @@ import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss
|
|||||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||||
|
|
||||||
import fs from 'node:fs'
|
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 { Target } from 'puppeteer'
|
||||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||||
import * as JSONStream from 'JSONStream'
|
import * as JSONStream from 'JSONStream'
|
||||||
@@ -31,38 +26,11 @@ import { type ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/src/entity/Ch
|
|||||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||||
|
|
||||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
import {
|
||||||
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
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 dbInitPromise = initDb(getPublicDbFilePath())
|
||||||
|
|
||||||
const attachRequestsListener = async (target: Target) => {
|
const attachRequestsListener = async (target: Target) => {
|
||||||
@@ -221,7 +189,7 @@ const attachRequestsListener = async (target: Target) => {
|
|||||||
mappedItem.mid = it.mid
|
mappedItem.mid = it.mid
|
||||||
mappedItem.encryptFromUserId = it.isSelf ? currentUserInfo.encryptUserId : bossInfo.encryptBossId
|
mappedItem.encryptFromUserId = it.isSelf ? currentUserInfo.encryptUserId : bossInfo.encryptBossId
|
||||||
mappedItem.encryptToUserId = it.isSelf ? bossInfo.encryptBossId : currentUserInfo.encryptUserId
|
mappedItem.encryptToUserId = it.isSelf ? bossInfo.encryptBossId : currentUserInfo.encryptUserId
|
||||||
mappedItem.style = it.style
|
mappedItem.style = it.isSelf ? 'sent' : 'received'
|
||||||
mappedItem.type = it.type
|
mappedItem.type = it.type
|
||||||
mappedItem.time = it.time ? new Date(it.time) : null
|
mappedItem.time = it.time ? new Date(it.time) : null
|
||||||
mappedItem.text = it.text
|
mappedItem.text = it.text
|
||||||
@@ -247,12 +215,7 @@ const attachRequestsListener = async (target: Target) => {
|
|||||||
|
|
||||||
export async function launchBossSite() {
|
export async function launchBossSite() {
|
||||||
app.dock?.hide()
|
app.dock?.hide()
|
||||||
if (!fs.existsSync(path.join(editThisCookieExtensionPath, 'manifest.json'))) {
|
await ensureEditThisCookie()
|
||||||
await extractZip(await getEditThisCookieZipPath(), {
|
|
||||||
dir: extensionDir
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const bossCookies = readStorageFile('boss-cookies.json')
|
const bossCookies = readStorageFile('boss-cookies.json')
|
||||||
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createMainWindow } from '../../window/mainWindow'
|
|||||||
import './app-menu'
|
import './app-menu'
|
||||||
import initIpc from './ipc'
|
import initIpc from './ipc'
|
||||||
import gtag from '../../utils/gtag'
|
import gtag from '../../utils/gtag'
|
||||||
|
import initPublicIpc from '../../utils/initPublicIpc'
|
||||||
export function openSettingWindow() {
|
export function openSettingWindow() {
|
||||||
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
||||||
if (!app.requestSingleInstanceLock()) {
|
if (!app.requestSingleInstanceLock()) {
|
||||||
@@ -31,6 +32,7 @@ export function openSettingWindow() {
|
|||||||
|
|
||||||
// IPC test
|
// IPC test
|
||||||
ipcMain.on('ping', () => console.log('pong'))
|
ipcMain.on('ping', () => console.log('pong'))
|
||||||
|
initPublicIpc()
|
||||||
initIpc()
|
initIpc()
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ipcMain, shell, app } from 'electron'
|
import { ipcMain, shell, app } from 'electron'
|
||||||
|
import path from 'path'
|
||||||
import * as childProcess from 'node:child_process'
|
import * as childProcess from 'node:child_process'
|
||||||
import {
|
import {
|
||||||
ensureConfigFileExist,
|
ensureConfigFileExist,
|
||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
readConfigFile,
|
readConfigFile,
|
||||||
writeConfigFile,
|
writeConfigFile,
|
||||||
readStorageFile,
|
readStorageFile,
|
||||||
writeStorageFile
|
writeStorageFile,
|
||||||
|
storageFilePath
|
||||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||||
import { ChildProcess } from 'child_process'
|
import { ChildProcess } from 'child_process'
|
||||||
import * as JSONStream from 'JSONStream'
|
import * as JSONStream from 'JSONStream'
|
||||||
@@ -30,14 +31,27 @@ import { pipeWriteRegardlessError } from '../../utils/pipe'
|
|||||||
import { WriteStream } from 'node:fs'
|
import { WriteStream } from 'node:fs'
|
||||||
// eslint-disable-next-line vue/prefer-import-from-vue
|
// eslint-disable-next-line vue/prefer-import-from-vue
|
||||||
import { hasOwn } from '@vue/shared'
|
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'
|
||||||
|
|
||||||
export default function initIpc() {
|
export default function initIpc() {
|
||||||
ipcMain.on('open-external-link', (_, link) => {
|
|
||||||
shell.openExternal(link, {
|
|
||||||
activate: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('fetch-config-file-content', async () => {
|
ipcMain.handle('fetch-config-file-content', async () => {
|
||||||
const configFileContentList = configFileNameList.map((fileName) => {
|
const configFileContentList = configFileNameList.map((fileName) => {
|
||||||
return readConfigFile(fileName)
|
return readConfigFile(fileName)
|
||||||
@@ -69,8 +83,15 @@ export default function initIpc() {
|
|||||||
if (hasOwn(payload, 'anyCombineRecommendJobFilter')) {
|
if (hasOwn(payload, 'anyCombineRecommendJobFilter')) {
|
||||||
bossConfig.anyCombineRecommendJobFilter = payload.anyCombineRecommendJobFilter
|
bossConfig.anyCombineRecommendJobFilter = payload.anyCombineRecommendJobFilter
|
||||||
}
|
}
|
||||||
if (hasOwn(payload, 'expectJobRegExpStr')) {
|
delete bossConfig.expectJobRegExpStr
|
||||||
bossConfig.expectJobRegExpStr = payload.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, 'autoReminder')) {
|
if (hasOwn(payload, 'autoReminder')) {
|
||||||
bossConfig.autoReminder = payload.autoReminder
|
bossConfig.autoReminder = payload.autoReminder
|
||||||
@@ -435,6 +456,130 @@ export default function initIpc() {
|
|||||||
return await getJobHistoryByEncryptId(encryptJobId)
|
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')
|
||||||
|
ipcMain.removeHandler('fetch-resume-content')
|
||||||
|
defer.reject(new Error('cancel'))
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('fetch-resume-content', async () => {
|
||||||
|
const res = (await readConfigFile('resumes.json'))?.[0]
|
||||||
|
return res?.content ?? null
|
||||||
|
})
|
||||||
|
return defer.promise
|
||||||
|
})
|
||||||
|
ipcMain.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()
|
||||||
|
)
|
||||||
|
|
||||||
ipcMain.handle('exit-app-immediately', () => {
|
ipcMain.handle('exit-app-immediately', () => {
|
||||||
app.exit(0)
|
app.exit(0)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ const payloadHandler = {
|
|||||||
const [data, totalItemCount] = await measureExecutionTime(
|
const [data, totalItemCount] = await measureExecutionTime(
|
||||||
userRepository.findAndCount({
|
userRepository.findAndCount({
|
||||||
skip: (pageNo - 1) * pageSize,
|
skip: (pageNo - 1) * pageSize,
|
||||||
take: pageSize
|
take: pageSize,
|
||||||
|
order: {
|
||||||
|
date: 'DESC'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@@ -69,7 +72,10 @@ const payloadHandler = {
|
|||||||
const [data, totalItemCount] = await measureExecutionTime(
|
const [data, totalItemCount] = await measureExecutionTime(
|
||||||
recordRepository.findAndCount({
|
recordRepository.findAndCount({
|
||||||
skip: (pageNo - 1) * pageSize,
|
skip: (pageNo - 1) * pageSize,
|
||||||
take: pageSize
|
take: pageSize,
|
||||||
|
order: {
|
||||||
|
date: 'DESC'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { Page } from 'puppeteer'
|
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) => {
|
export const sendLookForwardReplyEmotion = async (page: Page) => {
|
||||||
const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion')
|
const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion')
|
||||||
@@ -15,3 +26,241 @@ export const sendLookForwardReplyEmotion = async (page: Page) => {
|
|||||||
)
|
)
|
||||||
await lookForwardReplyEmojiProxy!.click()
|
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 { bootstrap, launchBoss } from './bootstrap'
|
||||||
import { MsgStatus, type ChatListItem } from './types'
|
import { MsgStatus, type ChatListItem } from './types'
|
||||||
import { Browser, Page } from 'puppeteer'
|
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 { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||||
import { app } from 'electron'
|
import { app, dialog } from 'electron'
|
||||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||||
import {
|
import {
|
||||||
getPublicDbFilePath,
|
getPublicDbFilePath,
|
||||||
@@ -17,10 +17,21 @@ import * as fs from 'fs'
|
|||||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||||
|
import { RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat'
|
||||||
|
import gtag from '../../utils/gtag'
|
||||||
|
|
||||||
const throttleIntervalMinutes =
|
const throttleIntervalMinutes =
|
||||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||||
const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21
|
const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21
|
||||||
|
const recentMessageQuantityForLlm =
|
||||||
|
readConfigFile('boss.json').autoReminder?.recentMessageQuantityForLlm ?? 8
|
||||||
|
const rechatContentSource =
|
||||||
|
readConfigFile('boss.json').autoReminder?.rechatContentSource ??
|
||||||
|
RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION
|
||||||
|
const rechatLlmFallback =
|
||||||
|
readConfigFile('boss.json').autoReminder?.rechatLlmFallback ??
|
||||||
|
RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||||
|
|
||||||
const dbInitPromise = initDb(getPublicDbFilePath())
|
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||||
|
|
||||||
export const pageMapByName: {
|
export const pageMapByName: {
|
||||||
@@ -172,6 +183,9 @@ const mainLoop = async () => {
|
|||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
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
|
// find target boss - with unread icon, or recommend system message
|
||||||
const friendListData = (await pageMapByName.boss!.evaluate(
|
const friendListData = (await pageMapByName.boss!.evaluate(
|
||||||
`
|
`
|
||||||
@@ -277,7 +291,31 @@ const mainLoop = async () => {
|
|||||||
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
|
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
|
||||||
) {
|
) {
|
||||||
await sleepWithRandomDelay(3250)
|
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 {
|
} else {
|
||||||
cursorToContinueFind += 1
|
cursorToContinueFind += 1
|
||||||
}
|
}
|
||||||
@@ -337,6 +375,17 @@ export async function runEntry() {
|
|||||||
)
|
)
|
||||||
process.exit(1)
|
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 {
|
} finally {
|
||||||
pageMapByName['boss'] = null
|
pageMapByName['boss'] = null
|
||||||
await sleep(rerunInterval)
|
await sleep(rerunInterval)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import buildInfo from '../../../common/build-info.json'
|
import buildInfo from '../../../common/build-info.json'
|
||||||
|
import os from 'node:os'
|
||||||
|
|
||||||
type LowercaseLetter =
|
type LowercaseLetter =
|
||||||
| 'a'
|
| 'a'
|
||||||
@@ -55,6 +56,7 @@ function getCommonParams() {
|
|||||||
return {
|
return {
|
||||||
app_version: buildInfo.version,
|
app_version: buildInfo.version,
|
||||||
app_build_hash: buildInfo.buildHash,
|
app_build_hash: buildInfo.buildHash,
|
||||||
|
os_info: `${os.type()} | ${os.release()} | ${os.arch()}`,
|
||||||
t: Number(new Date())
|
t: Number(new Date())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,9 +70,14 @@ export default async function gtag<T extends string>(
|
|||||||
...getCommonParams(),
|
...getCommonParams(),
|
||||||
...params
|
...params
|
||||||
}
|
}
|
||||||
|
Object.keys(params).forEach((k) => {
|
||||||
|
if ([null, undefined].includes(params[k])) {
|
||||||
|
delete params[k]
|
||||||
|
}
|
||||||
|
})
|
||||||
// ServiceWorker环境下直接调用上报函数
|
// ServiceWorker环境下直接调用上报函数
|
||||||
const reporter = (await import('./Analytics')).default
|
const reporter = (await import('./Analytics')).default
|
||||||
return reporter.fireEvent(name, params)
|
return reporter.fireEvent(name.replace(/-/g, '_'), params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire a page view event.
|
// Fire a page view event.
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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.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()
|
createFirstLaunchNoticeApproveFlag()
|
||||||
firstLaunchNoticeWindow?.close()
|
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()
|
initIpc()
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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,
|
||||||
|
frame: false,
|
||||||
|
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!
|
||||||
|
}
|
||||||
@@ -23,8 +23,6 @@ export function createMainWindow(): BrowserWindow {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
process.env.NODE_ENV === 'development' && openDevTools(mainWindow)
|
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
mainWindow.show()
|
mainWindow.show()
|
||||||
})
|
})
|
||||||
@@ -36,6 +34,12 @@ export function createMainWindow(): BrowserWindow {
|
|||||||
show: true
|
show: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
mainWindow.on('ready-to-show', async () => {
|
||||||
|
process.env.NODE_ENV === 'development' &&
|
||||||
|
setTimeout(() => {
|
||||||
|
mainWindow && openDevTools(mainWindow)
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url)
|
shell.openExternal(details.url)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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,
|
||||||
|
frame: false,
|
||||||
|
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!
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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,
|
||||||
|
// frame: false,
|
||||||
|
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">
|
<script setup lang="ts">
|
||||||
import { ElConfigProvider } from 'element-plus'
|
import { ElConfigProvider } from 'element-plus'
|
||||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
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>
|
</script>
|
||||||
|
|||||||
@@ -24,7 +24,11 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="diff-table-header">
|
<div class="diff-table-header">
|
||||||
{{ transformUtcDateToLocalDate(item.value).format('YYYY-MM-DD HH:mm:ss') }}
|
{{ 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-radio v-model="diffPivot" :label="item.value" :disabled="tableProps.length <= 1">作为diff基准</el-radio>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</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 { ElTable, ElTableColumn, ElForm, ElFormItem, ElRow, ElCol, ElDivider } from 'element-plus'
|
||||||
import TextDiff from '../../components/TextDiff.vue'
|
import TextDiff from '../../components/TextDiff.vue'
|
||||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
jobInfo: {
|
jobInfo: {
|
||||||
|
|||||||
@@ -3,9 +3,20 @@
|
|||||||
<el-form-item label="公司">{{ jobInfo.companyName }}</el-form-item>
|
<el-form-item label="公司">{{ jobInfo.companyName }}</el-form-item>
|
||||||
<el-form-item label="职位名称">{{ jobInfo.jobName }}</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="职位分类">{{ jobInfo.positionName }}</el-form-item>
|
||||||
<el-form-item label="开聊时间">{{
|
<el-form-item v-if="scene === 'startChatRecord'" label="开聊时间">
|
||||||
transformUtcDateToLocalDate(jobInfo.date).format('YYYY-MM-DD HH:mm:ss')
|
{{
|
||||||
}}</el-form-item>
|
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.experienceName }}</el-form-item>
|
||||||
<el-form-item label="薪资">{{
|
<el-form-item label="薪资">{{
|
||||||
`${jobInfo.salaryLow}-${jobInfo.salaryHigh}k` +
|
`${jobInfo.salaryLow}-${jobInfo.salaryHigh}k` +
|
||||||
@@ -14,19 +25,26 @@
|
|||||||
<el-form-item label="职位描述">
|
<el-form-item label="职位描述">
|
||||||
<pre class="of-auto">{{ jobInfo.description }}</pre>
|
<pre class="of-auto">{{ jobInfo.description }}</pre>
|
||||||
</el-form-item>
|
</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>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from 'vue'
|
import { PropType } from 'vue'
|
||||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
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'
|
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
jobInfo: {
|
jobInfo: {
|
||||||
type: Object as PropType<VChatStartupLog>,
|
type: Object as PropType<VChatStartupLog | VMarkAsNotSuitLog>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
type: String
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -33,10 +34,12 @@ const checkDependenciesResult = ref({})
|
|||||||
const downloadProcessWaitee = ref(null)
|
const downloadProcessWaitee = ref(null)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
gtagRenderer('bootstrap_mounted')
|
||||||
checkDependenciesResult.value = await electron.ipcRenderer.invoke('check-dependencies')
|
checkDependenciesResult.value = await electron.ipcRenderer.invoke('check-dependencies')
|
||||||
downloadProcessWaitee.value = Promise.withResolvers()
|
downloadProcessWaitee.value = Promise.withResolvers()
|
||||||
|
|
||||||
if (Object.values(checkDependenciesResult.value).includes(false)) {
|
if (Object.values(checkDependenciesResult.value).includes(false)) {
|
||||||
|
gtagRenderer('dependencies_need_download')
|
||||||
router.replace('/downloadingDependencies')
|
router.replace('/downloadingDependencies')
|
||||||
} else {
|
} else {
|
||||||
downloadProcessWaitee.value!.resolve()
|
downloadProcessWaitee.value!.resolve()
|
||||||
@@ -45,10 +48,11 @@ onMounted(async () => {
|
|||||||
downloadProcessWaitee.value!.promise.then(async () => {
|
downloadProcessWaitee.value!.promise.then(async () => {
|
||||||
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
|
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
|
||||||
if (!isCookieFileValid) {
|
if (!isCookieFileValid) {
|
||||||
|
gtagRenderer('found_cookie_invalid_when_bootstrap')
|
||||||
router.replace('/cookieAssistant')
|
router.replace('/cookieAssistant')
|
||||||
} else {
|
} else {
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
router.replace('/configuration')
|
router.replace('/main-layout')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onUnmounted, PropType } from 'vue'
|
import { ref, onUnmounted, PropType } from 'vue'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dependenciesStatus: {
|
dependenciesStatus: {
|
||||||
@@ -47,9 +48,11 @@ const processDownloadBrowser = async () => {
|
|||||||
const promiseList: Array<Promise<void>> = []
|
const promiseList: Array<Promise<void>> = []
|
||||||
const processTasks = async () => {
|
const processTasks = async () => {
|
||||||
if (!props.dependenciesStatus.puppeteerExecutableAvailable) {
|
if (!props.dependenciesStatus.puppeteerExecutableAvailable) {
|
||||||
|
gtagRenderer('start_download_puppeteer')
|
||||||
const p = processDownloadBrowser()
|
const p = processDownloadBrowser()
|
||||||
promiseList.push(p)
|
promiseList.push(p)
|
||||||
p.then(() => {
|
p.then(() => {
|
||||||
|
gtagRenderer('puppeteer_download_success')
|
||||||
props.dependenciesStatus.puppeteerExecutableAvailable = true
|
props.dependenciesStatus.puppeteerExecutableAvailable = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -64,6 +67,7 @@ const processTasks = async () => {
|
|||||||
})
|
})
|
||||||
await p
|
await p
|
||||||
} catch {
|
} catch {
|
||||||
|
gtagRenderer('encounter_error_when_download_deps')
|
||||||
await ElMessageBox.confirm('需要重试吗?', '核心组件下载失败', {
|
await ElMessageBox.confirm('需要重试吗?', '核心组件下载失败', {
|
||||||
closeOnClickModal: false,
|
closeOnClickModal: false,
|
||||||
closeOnPressEscape: false,
|
closeOnPressEscape: false,
|
||||||
@@ -72,9 +76,11 @@ const processTasks = async () => {
|
|||||||
cancelButtonText: '退出程序'
|
cancelButtonText: '退出程序'
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
gtagRenderer('start_retry_download_deps')
|
||||||
processTasks()
|
processTasks()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
gtagRenderer('cancel_download_deps_and_exit')
|
||||||
promiseList.length = 0
|
promiseList.length = 0
|
||||||
electron.ipcRenderer.invoke('exit-app-immediately')
|
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>
|
<template>
|
||||||
<div class="cookie-assistant-page">
|
<div class="cookie-assistant-page">
|
||||||
<div ml1em mt1em mb1em >Cookie 助手</div>
|
<div ml1em mt1em mb1em font-size-16px>Cookie 助手</div>
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="cookieInvalid"
|
v-if="cookieInvalid"
|
||||||
type="warning"
|
type="warning"
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
</el-alert>
|
</el-alert>
|
||||||
<div ml1em mt1em line-height-normal>
|
<div ml1em mt1em line-height-normal>
|
||||||
如果您了解如何获取Cookie、了解有效的Cookie格式,可以直接在下方输入框中进行编辑。由于手动编辑较为麻烦,建议您打开已登录过Boss直聘的浏览器,使用<a
|
如果您了解如何获取Cookie、了解有效的Cookie格式,可以直接在下方输入框中进行编辑。由于手动编辑较为麻烦,建议您打开已登录过Boss直聘的浏览器,使用<a
|
||||||
color-blue
|
class="color-blue! decoration-none"
|
||||||
decoration-none
|
|
||||||
href="javascript:void(0)"
|
href="javascript:void(0)"
|
||||||
@click.prevent="handleEditThisCookieExtensionStoreLinkClick"
|
@click.prevent="handleEditThisCookieExtensionStoreLinkClick"
|
||||||
>EditThisCookie 扩展程序</a
|
>
|
||||||
|
EditThisCookie 扩展程序 </a
|
||||||
>复制Cookie,然后粘贴在下方输入框中。文本格式为被序列化为JSON的数组,不含两侧引号。
|
>复制Cookie,然后粘贴在下方输入框中。文本格式为被序列化为JSON的数组,不含两侧引号。
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -85,7 +85,12 @@
|
|||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
font-size-inherit
|
font-size-inherit
|
||||||
@click="fillCollectedCookie"
|
@click="
|
||||||
|
() => {
|
||||||
|
gtagRenderer('replace_inputted_cookie_by_collected')
|
||||||
|
fillCollectedCookie()
|
||||||
|
}
|
||||||
|
"
|
||||||
>使用获取到的Cookie</el-button
|
>使用获取到的Cookie</el-button
|
||||||
></template
|
></template
|
||||||
></el-alert
|
></el-alert
|
||||||
@@ -103,7 +108,8 @@
|
|||||||
import { ElForm, ElMessage } from 'element-plus'
|
import { ElForm, ElMessage } from 'element-plus'
|
||||||
import { ref, onUnmounted, onMounted } from 'vue'
|
import { ref, onUnmounted, onMounted } from 'vue'
|
||||||
import { checkCookieListFormat } from '../../../../common/utils/cookie'
|
import { checkCookieListFormat } from '../../../../common/utils/cookie'
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const cookieInvalid = ref(false)
|
const cookieInvalid = ref(false)
|
||||||
@@ -138,11 +144,13 @@ const formRules = {
|
|||||||
`JSON格式无效 - 存在语法错误: ${err.message};建议使用EditThisCookie扩展程序进行复制。`
|
`JSON格式无效 - 存在语法错误: ${err.message};建议使用EditThisCookie扩展程序进行复制。`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
gtagRenderer('wrong_cookie_format_json_syntax_error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkCookieListFormat(JSON.parse(formContent.value.collectedCookies))) {
|
if (!checkCookieListFormat(JSON.parse(formContent.value.collectedCookies))) {
|
||||||
cb(new Error(`Cookie格式无效 - 部分字段缺失;建议使用EditThisCookie扩展程序进行复制。`))
|
cb(new Error(`Cookie格式无效 - 部分字段缺失;建议使用EditThisCookie扩展程序进行复制。`))
|
||||||
|
gtagRenderer('wrong_cookie_format_field_loss')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cb()
|
cb()
|
||||||
@@ -158,6 +166,9 @@ const handleCookieCollected = (_, payload) => {
|
|||||||
collectedCookie.value = payload.cookies
|
collectedCookie.value = payload.cookies
|
||||||
if (!hasUserMutateInput.value) {
|
if (!hasUserMutateInput.value) {
|
||||||
fillCollectedCookie()
|
fillCollectedCookie()
|
||||||
|
gtagRenderer('cookie_collected_and_auto_filled')
|
||||||
|
} else {
|
||||||
|
gtagRenderer('cookie_collected_after_changed_input')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fillCollectedCookie = () => {
|
const fillCollectedCookie = () => {
|
||||||
@@ -169,28 +180,33 @@ const fillCollectedCookie = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleClickLaunchLogin = () => {
|
const handleClickLaunchLogin = () => {
|
||||||
|
gtagRenderer('launch_login_button_clicked')
|
||||||
electron.ipcRenderer.send('launch-bosszhipin-login-page-with-preload-extension')
|
electron.ipcRenderer.send('launch-bosszhipin-login-page-with-preload-extension')
|
||||||
loginCookieWaitingStatus.value = LOGIN_COOKIE_WAITING_STATUS.WAITING_FOR_LOGIN
|
loginCookieWaitingStatus.value = LOGIN_COOKIE_WAITING_STATUS.WAITING_FOR_LOGIN
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditThisCookieExtensionStoreLinkClick = () => {
|
const handleEditThisCookieExtensionStoreLinkClick = () => {
|
||||||
|
gtagRenderer('etc_extension_link_clicked')
|
||||||
electron.ipcRenderer.send(
|
electron.ipcRenderer.send(
|
||||||
'open-external-link',
|
'open-external-link',
|
||||||
'https://chromewebstore.google.com/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg'
|
'https://chromewebstore.google.com/detail/editthiscookie-v3/ojfebgpkimhlhcblbalbfjblapadhbol'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
router.replace('/configuration')
|
gtagRenderer('cancel_clicked')
|
||||||
|
router.replace('/main-layout')
|
||||||
}
|
}
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
gtagRenderer('save_clicked')
|
||||||
await formRef.value!.validate()
|
await formRef.value!.validate()
|
||||||
await electron.ipcRenderer.invoke('write-storage-file', {
|
await electron.ipcRenderer.invoke('write-storage-file', {
|
||||||
fileName: 'boss-cookies.json',
|
fileName: 'boss-cookies.json',
|
||||||
data: formContent.value.collectedCookies
|
data: formContent.value.collectedCookies
|
||||||
})
|
})
|
||||||
ElMessage.success('Boss直聘 Cookie 保存成功')
|
ElMessage.success('Boss直聘 Cookie 保存成功')
|
||||||
router.replace('/configuration')
|
gtagRenderer('save_cookie_done')
|
||||||
|
router.replace('/main-layout')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBossZhipinLoginPageClosed = () => {
|
const handleBossZhipinLoginPageClosed = () => {
|
||||||
@@ -199,6 +215,9 @@ const handleBossZhipinLoginPageClosed = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
gtagRenderer('cookie_assistant_mounted')
|
||||||
|
})
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
electron.ipcRenderer.once('BOSS_ZHIPIN_COOKIE_COLLECTED', handleCookieCollected)
|
electron.ipcRenderer.once('BOSS_ZHIPIN_COOKIE_COLLECTED', handleCookieCollected)
|
||||||
electron.ipcRenderer.on('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED', handleBossZhipinLoginPageClosed)
|
electron.ipcRenderer.on('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED', handleBossZhipinLoginPageClosed)
|
||||||
@@ -226,6 +245,7 @@ onUnmounted(() => {
|
|||||||
.cookie-assistant-page {
|
.cookie-assistant-page {
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus'
|
import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus'
|
||||||
import { ref, onMounted, onBeforeMount } from 'vue';
|
import { ref, onMounted, onBeforeMount } from 'vue';
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const electron = window.electron
|
const electron = window.electron
|
||||||
|
|
||||||
const readmeItemCheckStatusList = ref<number[]>([])
|
const readmeItemCheckStatusList = ref<number[]>([])
|
||||||
@@ -97,8 +99,10 @@ const handleCancel = () => {
|
|||||||
|
|
||||||
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
|
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
gtagRenderer('submit_clicked')
|
||||||
const COUNT = 9
|
const COUNT = 9
|
||||||
if (readmeItemCheckStatusList.value.length !== COUNT) {
|
if (readmeItemCheckStatusList.value.length !== COUNT) {
|
||||||
|
gtagRenderer('agreement_not_finish_read_tip_displayed')
|
||||||
ElMessage.warning(
|
ElMessage.warning(
|
||||||
`您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`
|
`您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`
|
||||||
)
|
)
|
||||||
@@ -111,6 +115,7 @@ const handleSubmit = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
electron.ipcRenderer.invoke('first-launch-notice-approve')
|
electron.ipcRenderer.invoke('first-launch-notice-approve')
|
||||||
|
gtagRenderer('submit_done')
|
||||||
}
|
}
|
||||||
const handleReadmeItemCheckStatusListChange = (value: number[]) => {
|
const handleReadmeItemCheckStatusListChange = (value: number[]) => {
|
||||||
value.forEach((it) => {
|
value.forEach((it) => {
|
||||||
|
|||||||
@@ -18,22 +18,26 @@ import { ref, onUnmounted, onMounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import FlyingCompanyLogoList from '../../features/FlyingCompanyLogoList/index.vue'
|
import FlyingCompanyLogoList from '../../features/FlyingCompanyLogoList/index.vue'
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const { ipcRenderer } = electron
|
const { ipcRenderer } = electron
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleStopButtonClick = async () => {
|
const handleStopButtonClick = async () => {
|
||||||
|
gtagRenderer('gascwb_stop_button_clicked')
|
||||||
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
|
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStopping = ref(false)
|
const isStopping = ref(false)
|
||||||
const handleStopping = () => {
|
const handleStopping = () => {
|
||||||
|
gtagRenderer('gascwb_become_stopping')
|
||||||
isStopping.value = true
|
isStopping.value = true
|
||||||
}
|
}
|
||||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
||||||
|
|
||||||
const handleStopped = () => {
|
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)
|
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')
|
await electron.ipcRenderer.invoke('run-geek-auto-start-chat-with-boss')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
||||||
|
gtagRenderer('gascwb_cannot_run_for_corrupt')
|
||||||
ElMessage.error({
|
ElMessage.error({
|
||||||
message: `核心组件损坏,正在尝试修复`
|
message: `核心组件损坏,正在尝试修复`
|
||||||
})
|
})
|
||||||
router.replace('/')
|
router.replace('/')
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
gtagRenderer('gascwb_cannot_run_for_unknown_error', { err })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+7
-1
@@ -16,22 +16,26 @@ import { ref, onUnmounted, onMounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import FlyingCompanyLogoList from '../../features/FlyingCompanyLogoList/index.vue'
|
import FlyingCompanyLogoList from '../../features/FlyingCompanyLogoList/index.vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const { ipcRenderer } = electron
|
const { ipcRenderer } = electron
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleStopButtonClick = async () => {
|
const handleStopButtonClick = async () => {
|
||||||
|
gtagRenderer('rnrr_stop_button_clicked')
|
||||||
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
|
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStopping = ref(false)
|
const isStopping = ref(false)
|
||||||
const handleStopping = () => {
|
const handleStopping = () => {
|
||||||
|
gtagRenderer('rnrr_become_stopping')
|
||||||
isStopping.value = true
|
isStopping.value = true
|
||||||
}
|
}
|
||||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
||||||
|
|
||||||
const handleStopped = () => {
|
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)
|
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')
|
await electron.ipcRenderer.invoke('run-read-no-reply-auto-reminder')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
||||||
|
gtagRenderer('rnrr_cannot_run_for_corrupt')
|
||||||
ElMessage.error({
|
ElMessage.error({
|
||||||
message: `核心组件损坏,正在尝试修复`
|
message: `核心组件损坏,正在尝试修复`
|
||||||
})
|
})
|
||||||
router.replace('/')
|
router.replace('/')
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
gtagRenderer('rnrr_cannot_run_for_unknown_error', { err })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,563 @@
|
|||||||
|
<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'
|
||||||
|
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,
|
||||||
|
(nVal) => {
|
||||||
|
if (nVal <= 1) {
|
||||||
|
electron.ipcRenderer.send('update-window-size', {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: 550
|
||||||
|
})
|
||||||
|
} 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>
|
||||||
+25
-1
@@ -19,7 +19,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||||
<div class="w100px">
|
<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>
|
</div>
|
||||||
<ElPagination
|
<ElPagination
|
||||||
v-model:current-page="pagination.pageNo"
|
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 { ElTable, ElTableColumn, ElButton, ElPagination } from 'element-plus'
|
||||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
||||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const tableData = ref<VChatStartupLog[]>([])
|
const tableData = ref<VChatStartupLog[]>([])
|
||||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||||
@@ -54,6 +65,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
|||||||
const isTableLoading = ref(false)
|
const isTableLoading = ref(false)
|
||||||
async function getBossLibrary() {
|
async function getBossLibrary() {
|
||||||
try {
|
try {
|
||||||
|
gtagRenderer('boss_library_request_sent', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
isTableLoading.value = true
|
isTableLoading.value = true
|
||||||
const { data: res } = (await electron.ipcRenderer.invoke('get-boss-library', {
|
const { data: res } = (await electron.ipcRenderer.invoke('get-boss-library', {
|
||||||
pageNo: pagination.value.pageNo,
|
pageNo: pagination.value.pageNo,
|
||||||
@@ -65,7 +80,16 @@ async function getBossLibrary() {
|
|||||||
pageNo: res.pageNo,
|
pageNo: res.pageNo,
|
||||||
pageSize: pagination.value.pageSize
|
pageSize: pagination.value.pageSize
|
||||||
}
|
}
|
||||||
|
gtagRenderer('boss_library_request_success', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
gtagRenderer('boss_library_request_error', {
|
||||||
|
err,
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
console.log(err)
|
console.log(err)
|
||||||
tableData.value = []
|
tableData.value = []
|
||||||
} finally {
|
} finally {
|
||||||
+23
-1
@@ -23,7 +23,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||||
<div class="w100px">
|
<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
|
>刷新</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,6 +57,7 @@ import { ElTable, ElTableColumn, ElButton, ElPagination } from 'element-plus'
|
|||||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
||||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||||
import { formatCompanyScale } from '@geekgeekrun/sqlite-plugin/src/utils/parser'
|
import { formatCompanyScale } from '@geekgeekrun/sqlite-plugin/src/utils/parser'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const tableData = ref<VChatStartupLog[]>([])
|
const tableData = ref<VChatStartupLog[]>([])
|
||||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||||
@@ -64,6 +73,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
|||||||
const isTableLoading = ref(false)
|
const isTableLoading = ref(false)
|
||||||
async function getCompanyLibrary() {
|
async function getCompanyLibrary() {
|
||||||
try {
|
try {
|
||||||
|
gtagRenderer('company_library_request_sent', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
isTableLoading.value = true
|
isTableLoading.value = true
|
||||||
const { data: res } = (await electron.ipcRenderer.invoke('get-company-library', {
|
const { data: res } = (await electron.ipcRenderer.invoke('get-company-library', {
|
||||||
pageNo: pagination.value.pageNo,
|
pageNo: pagination.value.pageNo,
|
||||||
@@ -75,7 +88,16 @@ async function getCompanyLibrary() {
|
|||||||
pageNo: res.pageNo,
|
pageNo: res.pageNo,
|
||||||
pageSize: pagination.value.pageSize
|
pageSize: pagination.value.pageSize
|
||||||
}
|
}
|
||||||
|
gtagRenderer('company_library_request_success', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
gtagRenderer('company_library_request_error', {
|
||||||
|
err,
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
console.log(err)
|
console.log(err)
|
||||||
tableData.value = []
|
tableData.value = []
|
||||||
} finally {
|
} finally {
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-wrap geek-auto-start-run-with-boss">
|
||||||
|
<el-form ref="formRef" :model="formContent" label-position="top" :rules="formRules">
|
||||||
|
<el-form-item label="BOSS直聘 Cookie">
|
||||||
|
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
|
||||||
|
>编辑Cookie</el-button
|
||||||
|
>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- <el-form-item
|
||||||
|
label="钉钉机器人 AccessToken(用于记录开聊,请勿使用公司内部群)"
|
||||||
|
prop="dingtalkRobotAccessToken"
|
||||||
|
>
|
||||||
|
<el-input v-model="formContent.dingtalkRobotAccessToken" />
|
||||||
|
</el-form-item> -->
|
||||||
|
<div>
|
||||||
|
<el-form-item mb0>
|
||||||
|
<div>
|
||||||
|
是否查看职位详情的条件
|
||||||
|
<span font-size-12px>(以下条件为空表示不筛选)</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="expectCompanies" mb10px>
|
||||||
|
<div
|
||||||
|
v-full
|
||||||
|
font-size-12px
|
||||||
|
flex
|
||||||
|
:style="{
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
期望公司(以逗号分隔,不区分大小写)<el-tooltip
|
||||||
|
effect="light"
|
||||||
|
placement="bottom-start"
|
||||||
|
@show="gtagRenderer('tooltip_show_about_expect_company_figure')"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<img block h-270px src="./resources/intro-of-job-entry.png" />
|
||||||
|
</template>
|
||||||
|
<el-button type="text" font-size-12px
|
||||||
|
><span><QuestionFilled w-1em h-1em mr2px /></span>期望公司信息位置图示</el-button
|
||||||
|
>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<el-dropdown @command="handleExpectCompanyTemplateClicked">
|
||||||
|
<el-button size="small"
|
||||||
|
>期望公司模板 <el-icon class="el-icon--right"><arrow-down /></el-icon
|
||||||
|
></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="item in expectCompanyTemplateList"
|
||||||
|
:key="item.name"
|
||||||
|
:command="item"
|
||||||
|
>{{ item.name }}</el-dropdown-item
|
||||||
|
>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-model="formContent.expectCompanies"
|
||||||
|
:autosize="{ minRows: 4 }"
|
||||||
|
max-h-8lh
|
||||||
|
type="textarea"
|
||||||
|
@blur="normalizeExpectCompanies"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div mb42px>
|
||||||
|
<el-form-item mb0>
|
||||||
|
查看职位详情后,是发起投递还是标记不合适的条件
|
||||||
|
<span font-size-12px>(以下条件为空表示不筛选)</span>
|
||||||
|
</el-form-item>
|
||||||
|
<div
|
||||||
|
flex
|
||||||
|
:style="{
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<el-tooltip
|
||||||
|
effect="light"
|
||||||
|
placement="bottom"
|
||||||
|
@show="gtagRenderer('tooltip_show_about_expect_job_info_figure')"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<img block h-270px src="./resources/intro-of-job-info.png" />
|
||||||
|
</template>
|
||||||
|
<el-button type="text" font-size-12px
|
||||||
|
><span><QuestionFilled w-1em h-1em mr2px /></span>如下各信息位置图示</el-button
|
||||||
|
>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip
|
||||||
|
effect="light"
|
||||||
|
placement="bottom-start"
|
||||||
|
@show="gtagRenderer('tooltip_show_about_mark_not_suit_intro')"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<ol m0 line-height-1.5em w-400px pl2em>
|
||||||
|
<li>
|
||||||
|
如果查找到的职位,职位名称、职位类型、职位描述与如下正则不匹配,则这个职位将被标记为不合适
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
如果查找到的职位活跃时间为“本月活跃”或更往前的时间,则这个职位将被标记为不合适
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
如有错误标记,请在左侧“<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
style="color: var(--el-color-primary)"
|
||||||
|
@click.prevent="
|
||||||
|
() => {
|
||||||
|
gtagRenderer('click_view_mansr_from_boss_b_tooltip')
|
||||||
|
$router.push('/main-layout/MarkAsNotSuitRecord')
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>标记不合适</a
|
||||||
|
>”记录中找到相关记录,手动对这些职位发起会话
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</template>
|
||||||
|
<el-button type="text" font-size-12px
|
||||||
|
><span><QuestionFilled w-1em h-1em mr2px /></span>标记不合适机制</el-button
|
||||||
|
>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<el-dropdown ml20px @command="handleExpectJobFilterTemplateClicked">
|
||||||
|
<el-button size="small"
|
||||||
|
>职位详情筛选模板(按职类区分)
|
||||||
|
<el-icon class="el-icon--right"><arrow-down /></el-icon
|
||||||
|
></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="item in expectJobFilterTemplateList"
|
||||||
|
:key="item.name"
|
||||||
|
:command="item"
|
||||||
|
>{{ item.name }}</el-dropdown-item
|
||||||
|
>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1em 1fr 1em 1fr',
|
||||||
|
gap: '5px',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'end'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<el-form-item mb0 prop="expectJobNameRegExpStr">
|
||||||
|
<div font-size-12px>职位名称正则(不区分大小写)</div>
|
||||||
|
<el-input
|
||||||
|
v-model="formContent.expectJobNameRegExpStr"
|
||||||
|
@blur="
|
||||||
|
formContent.expectJobNameRegExpStr =
|
||||||
|
formContent.expectJobNameRegExpStr?.trim() ?? ''
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<div mb10px font-size-12px flex flex-justify-center>且</div>
|
||||||
|
<el-form-item mb0 prop="expectJobTypeRegExpStr">
|
||||||
|
<div font-size-12px>职位类型正则(不区分大小写)</div>
|
||||||
|
<el-input
|
||||||
|
v-model="formContent.expectJobTypeRegExpStr"
|
||||||
|
@blur="
|
||||||
|
formContent.expectJobTypeRegExpStr =
|
||||||
|
formContent.expectJobTypeRegExpStr?.trim() ?? ''
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<div mb10px font-size-12px flex flex-justify-center>且</div>
|
||||||
|
<el-form-item mb0 prop="expectJobDescRegExpStr">
|
||||||
|
<div font-size-12px>职位描述正则(不区分大小写)</div>
|
||||||
|
<el-input
|
||||||
|
v-model="formContent.expectJobDescRegExpStr"
|
||||||
|
@blur="
|
||||||
|
formContent.expectJobDescRegExpStr =
|
||||||
|
formContent.expectJobDescRegExpStr?.trim() ?? ''
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-form-item
|
||||||
|
label="职位备选筛选条件(当前求职期望无合适职位时,自动更改Boss筛选条件,查找新工作)"
|
||||||
|
prop="filter"
|
||||||
|
mb0
|
||||||
|
>
|
||||||
|
<AnyCombineBossRecommendFilter v-model="formContent.anyCombineRecommendJobFilter" />
|
||||||
|
<div>
|
||||||
|
当前组合条件数:{{ currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString() }}
|
||||||
|
<span
|
||||||
|
v-if="currentAnyCombineRecommendJobFilterCombinationCount >= 20"
|
||||||
|
class="color-orange"
|
||||||
|
>不建议选择太多组合条件</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="last-form-item mb0">
|
||||||
|
<el-button @click="handleSave">仅保存配置</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit"> 保存配置,并开始求职! </el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { ElForm, ElMessage } from 'element-plus'
|
||||||
|
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import AnyCombineBossRecommendFilter from '@renderer/features/AnyCombineBossRecommendFilter/index.vue'
|
||||||
|
import { calculateTotalCombinations } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
import defaultTargetCompanyListConf from '@geekgeekrun/geek-auto-start-chat-with-boss/default-config-file/target-company-list.json'
|
||||||
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formContent = ref({
|
||||||
|
dingtalkRobotAccessToken: '',
|
||||||
|
expectCompanies: '',
|
||||||
|
anyCombineRecommendJobFilter: {},
|
||||||
|
expectJobNameRegExpStr: '',
|
||||||
|
expectJobTypeRegExpStr: '',
|
||||||
|
expectJobDescRegExpStr: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentAnyCombineRecommendJobFilterCombinationCount = computed(() => {
|
||||||
|
return calculateTotalCombinations(formContent.value.anyCombineRecommendJobFilter)
|
||||||
|
})
|
||||||
|
|
||||||
|
electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
|
||||||
|
console.log(res)
|
||||||
|
formContent.value.dingtalkRobotAccessToken = res.config['dingtalk.json']['groupRobotAccessToken']
|
||||||
|
formContent.value.expectCompanies = res.config['target-company-list.json'].join(',')
|
||||||
|
formContent.value.anyCombineRecommendJobFilter = res.config['boss.json']
|
||||||
|
?.anyCombineRecommendJobFilter ?? {
|
||||||
|
salaryList: [],
|
||||||
|
experienceList: [],
|
||||||
|
degreeList: [],
|
||||||
|
scaleList: [],
|
||||||
|
industryList: []
|
||||||
|
}
|
||||||
|
//
|
||||||
|
if (
|
||||||
|
res.config['boss.json']?.expectJobRegExpStr &&
|
||||||
|
typeof res.config['boss.json']?.expectJobNameRegExpStr === 'undefined' &&
|
||||||
|
typeof res.config['boss.json']?.expectJobTypeRegExpStr === 'undefined' &&
|
||||||
|
typeof res.config['boss.json']?.expectJobDescRegExpStr === 'undefined'
|
||||||
|
) {
|
||||||
|
res.config['boss.json'].expectJobNameRegExpStr = res.config['boss.json'].expectJobRegExpStr
|
||||||
|
res.config['boss.json'].expectJobTypeRegExpStr = res.config['boss.json'].expectJobRegExpStr
|
||||||
|
res.config['boss.json'].expectJobDescRegExpStr = res.config['boss.json'].expectJobRegExpStr
|
||||||
|
}
|
||||||
|
formContent.value.expectJobNameRegExpStr = res.config['boss.json'].expectJobNameRegExpStr?.trim()
|
||||||
|
formContent.value.expectJobTypeRegExpStr = res.config['boss.json'].expectJobTypeRegExpStr?.trim()
|
||||||
|
formContent.value.expectJobDescRegExpStr = res.config['boss.json'].expectJobDescRegExpStr?.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
expectJobNameRegExpStr: {
|
||||||
|
validator(_, value, cb) {
|
||||||
|
if (!value) {
|
||||||
|
cb()
|
||||||
|
gtagRenderer('empty_reg_exp_for_expect_job_name')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new RegExp(value, 'ig')
|
||||||
|
gtagRenderer('valid_reg_exp_for_expect_job_name')
|
||||||
|
cb()
|
||||||
|
} catch (err) {
|
||||||
|
cb(new Error(`正则无效:${err?.message}`))
|
||||||
|
gtagRenderer('invalid_reg_exp_for_expect_job_name')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectJobTypeRegExpStr: {
|
||||||
|
validator(_, value, cb) {
|
||||||
|
if (!value) {
|
||||||
|
cb()
|
||||||
|
gtagRenderer('empty_reg_exp_for_expect_job_type')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new RegExp(value, 'ig')
|
||||||
|
gtagRenderer('valid_reg_exp_for_expect_job_type')
|
||||||
|
cb()
|
||||||
|
} catch (err) {
|
||||||
|
cb(new Error(`正则无效:${err?.message}`))
|
||||||
|
gtagRenderer('invalid_reg_exp_for_expect_job_type')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectJobDescRegExpStr: {
|
||||||
|
validator(_, value, cb) {
|
||||||
|
if (!value) {
|
||||||
|
cb()
|
||||||
|
gtagRenderer('empty_reg_exp_for_expect_job_desc')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new RegExp(value, 'ig')
|
||||||
|
gtagRenderer('valid_reg_exp_for_expect_job_desc')
|
||||||
|
cb()
|
||||||
|
} catch (err) {
|
||||||
|
cb(new Error(`正则无效:${err?.message}`))
|
||||||
|
gtagRenderer('invalid_reg_exp_for_expect_job_desc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
gtagRenderer('save_config_and_launch_clicked', {
|
||||||
|
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
|
||||||
|
})
|
||||||
|
formContent.value.expectJobRegExpStr = undefined
|
||||||
|
try {
|
||||||
|
await formRef.value!.validate()
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error({
|
||||||
|
message: '表单校验失败,请检查有误的内容',
|
||||||
|
grouping: true
|
||||||
|
})
|
||||||
|
console.log(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
|
||||||
|
router.replace({
|
||||||
|
path: '/geekAutoStartChatWithBoss/prepareRun',
|
||||||
|
query: { flow: 'geek-auto-start-chat-with-boss' }
|
||||||
|
})
|
||||||
|
gtagRenderer('config_saved_and_launch_auto_start_chat', {
|
||||||
|
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const handleSave = async () => {
|
||||||
|
gtagRenderer('save_config_clicked', {
|
||||||
|
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
|
||||||
|
})
|
||||||
|
normalizeExpectCompanies()
|
||||||
|
try {
|
||||||
|
await formRef.value!.validate()
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error({
|
||||||
|
message: '表单校验失败,请检查有误的内容',
|
||||||
|
grouping: true
|
||||||
|
})
|
||||||
|
console.log(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
|
||||||
|
ElMessage.success('配置保存成功')
|
||||||
|
gtagRenderer('config_saved')
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeExpectCompanies = () => {
|
||||||
|
formContent.value.expectCompanies = formContent.value.expectCompanies
|
||||||
|
.split(/,|,/)
|
||||||
|
.map((it) => it.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickLaunchLogin = () => {
|
||||||
|
gtagRenderer('launch_login_clicked')
|
||||||
|
router.replace('/cookieAssistant')
|
||||||
|
}
|
||||||
|
const expectCompanyTemplateList = [
|
||||||
|
{
|
||||||
|
name: '默认值',
|
||||||
|
value: defaultTargetCompanyListConf.join(',')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '不限公司(随便投)',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '大厂及关联企业',
|
||||||
|
value: `抖音,字节,字跳,有竹居,脸萌,头条,懂车帝,滴滴,嘀嘀,小桔,网易,有道,腾讯,酷狗,酷我,阅文,搜狗,京东,沃东天骏,达达,达冠,百度,度小满,爱奇艺,携程,趣拿,去哪儿,集度,理想,蔚来,顺丰,讯飞,同程,艺龙,马蜂窝,贝壳,自如,链家,我爱我家,相寓,多点,金山,小米,猎豹,新浪,微博,阿里,蚂蚁,飞猪,乌鸫,饿了么,LAZADA,来赞达,飞猪,菜鸟,哈啰,钉钉,高德,美团,三快,猫眼,快手,映客,小红书,行吟,奇虎,360,三六零,鸿盈,奇富,奇元,亚信,启明星辰,奇安信,深信服,长亭,绿盟,天融信,商汤,SenseTime,大华,海康威视,hikvision,汽车之家,车好多,瓜子,易车,昆仑万维,昆仑天工,闲徕,趣加,FunPlus,完美,马上消费,轻松,水滴,白龙马,58,车欢欢,五八,红布林,致美,快狗,天鹅到家,转转,美餐,知乎,智者四海,易点云,搜狐,用友,畅捷通,猿辅导,小猿,猿力,好未来,学而思,希望学,新东方,东方甄选,东方优选,作业帮,高途,跟谁学,学科网,天学网,一起教育,一起作业,美术宝,火花思维,粉笔,老虎国际,一心向上,向上一意,联想,拉勾,乐视,欢聚,竞技世界,拼多多,寻梦,得物,Moka,希瑞亚斯,北森,OPPO,欧珀,vivo,维沃,小天才,步步高,读书郎,货拉拉,陌陌,探探,Shopee,首汽租车,神州租车,天眼查,旷视,小冰,美图,智谱华章,MiniMax,石头科技,迅雷,TP,希音,SHEIN,稀宇,深言,百川智能,与爱为舞,牵手`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '阿里系',
|
||||||
|
value: `阿里,蚂蚁,飞猪,乌鸫,饿了么,LAZADA,来赞达,菜鸟,哈啰,钉钉,高德,白龙马,新浪,微博`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '字节(头条/抖音)系',
|
||||||
|
value: `抖音,字节,字跳,有竹居,脸萌,头条,懂车帝`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '百度系',
|
||||||
|
value: `百度,度小满,爱奇艺,携程,趣拿,集度,作业帮`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '腾讯系',
|
||||||
|
value: `腾讯,酷狗,酷我,阅文,搜狗,京东,沃东天骏,达达,达冠,美团,三快,猫眼,快手,拼多多,寻梦,Shopee,滴滴,嘀嘀,小桔`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '外包、劳务派遣企业',
|
||||||
|
value: `青钱,软通动力,南天,睿服,中电金信,佰钧成,云链,博彦,汉克时代,柯莱特,拓保,亿达信息,纬创,微创,微澜,诚迈科技,法本,兆尹,诚迈,联合永道,新致软件,宇信科技,华为,德科,FESCO,科锐,科之锐`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
function handleExpectCompanyTemplateClicked(item) {
|
||||||
|
gtagRenderer('expect_company_tpl_clicked', {
|
||||||
|
name: item.name
|
||||||
|
})
|
||||||
|
formContent.value.expectCompanies = item.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectJobFilterTemplateList = [
|
||||||
|
{
|
||||||
|
name: '不限职位(随便投)',
|
||||||
|
config: {
|
||||||
|
expectJobNameRegExpStr: '',
|
||||||
|
expectJobTypeRegExpStr: '',
|
||||||
|
expectJobDescRegExpStr: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '研发 - 前端开发工程师',
|
||||||
|
config: {
|
||||||
|
expectJobNameRegExpStr: '前端|H5|FE',
|
||||||
|
expectJobTypeRegExpStr: '前端开发|javascript',
|
||||||
|
expectJobDescRegExpStr: '前端|vue|react|node|js|javascript|H5'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '研发 - Java',
|
||||||
|
config: {
|
||||||
|
expectJobNameRegExpStr: 'Java',
|
||||||
|
expectJobTypeRegExpStr: 'Java',
|
||||||
|
expectJobDescRegExpStr: 'JVM|Java|消息队列|MySQL|Nginx|Redis|Dubbo'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '人力 - 员工关系',
|
||||||
|
config: {
|
||||||
|
expectJobNameRegExpStr: '员工关系|劳动关系|SSC|人力资源|人资',
|
||||||
|
expectJobTypeRegExpStr: '员工关系|人力资源',
|
||||||
|
expectJobDescRegExpStr: '社保|考勤|入职|离职'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '人力 - 招聘',
|
||||||
|
config: {
|
||||||
|
expectJobNameRegExpStr: '招聘|招聘HR|招聘专员|招聘顾问|招聘专家|Recruiter|人力资源|人资',
|
||||||
|
expectJobTypeRegExpStr: '招聘|人力资源|猎头顾问',
|
||||||
|
expectJobDescRegExpStr: '简历筛选|面试安排|offer|猎头'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
function handleExpectJobFilterTemplateClicked(item) {
|
||||||
|
gtagRenderer('expect_job_filter_tpl_clicked', {
|
||||||
|
name: item.name
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.assign(formContent.value, {
|
||||||
|
...item.config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.form-wrap {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1000px;
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
:deep(.el-form) {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.last-form-item {
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
margin-top: 0px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.form-wrap.geek-auto-start-run-with-boss {
|
||||||
|
.el-form-item__error.el-form-item__error {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+38
-3
@@ -55,7 +55,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||||
<div class="w100px">
|
<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>
|
</div>
|
||||||
<ElPagination
|
<ElPagination
|
||||||
v-model:current-page="pagination.pageNo"
|
v-model:current-page="pagination.pageNo"
|
||||||
@@ -74,7 +84,13 @@
|
|||||||
<JobInfoSnapshot
|
<JobInfoSnapshot
|
||||||
v-if="selectedJobInfoForViewSnapshot"
|
v-if="selectedJobInfoForViewSnapshot"
|
||||||
:job-info="selectedJobInfoForViewSnapshot"
|
:job-info="selectedJobInfoForViewSnapshot"
|
||||||
@closed="selectedJobInfoForViewSnapshot = null"
|
scene="jobLibrary"
|
||||||
|
@closed="
|
||||||
|
() => {
|
||||||
|
gtagRenderer('job_info_snapshot_closed')
|
||||||
|
selectedJobInfoForViewSnapshot = null
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</ElDrawer>
|
</ElDrawer>
|
||||||
<ElDialog
|
<ElDialog
|
||||||
@@ -92,6 +108,7 @@
|
|||||||
:job-info-history-list="selectedJobHistory ?? []"
|
:job-info-history-list="selectedJobHistory ?? []"
|
||||||
@closed="
|
@closed="
|
||||||
() => {
|
() => {
|
||||||
|
gtagRenderer('job_library_list_dialog_closed')
|
||||||
selectedJobInfoForViewHistory = null
|
selectedJobInfoForViewHistory = null
|
||||||
selectedJobHistory = 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 { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||||
import JobInfoHistoryList from '../../features/JobInfoHistoryList/index.vue'
|
import JobInfoHistoryList from '../../features/JobInfoHistoryList/index.vue'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const tableData = ref<VChatStartupLog[]>([])
|
const tableData = ref<VChatStartupLog[]>([])
|
||||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||||
@@ -121,6 +139,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
|||||||
const isTableLoading = ref(false)
|
const isTableLoading = ref(false)
|
||||||
async function getJobLibrary() {
|
async function getJobLibrary() {
|
||||||
try {
|
try {
|
||||||
|
gtagRenderer('job_library_request_sent', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
isTableLoading.value = true
|
isTableLoading.value = true
|
||||||
const { data: res } = (await electron.ipcRenderer.invoke('get-job-library', {
|
const { data: res } = (await electron.ipcRenderer.invoke('get-job-library', {
|
||||||
pageNo: pagination.value.pageNo,
|
pageNo: pagination.value.pageNo,
|
||||||
@@ -132,7 +154,16 @@ async function getJobLibrary() {
|
|||||||
pageNo: res.pageNo,
|
pageNo: res.pageNo,
|
||||||
pageSize: pagination.value.pageSize
|
pageSize: pagination.value.pageSize
|
||||||
}
|
}
|
||||||
|
gtagRenderer('job_library_request_success', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
gtagRenderer('job_library_request_error', {
|
||||||
|
err,
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
console.log(err)
|
console.log(err)
|
||||||
tableData.value = []
|
tableData.value = []
|
||||||
} finally {
|
} finally {
|
||||||
@@ -160,10 +191,12 @@ const drawVisibleModelValue = ref(false)
|
|||||||
const selectedJobInfoForViewSnapshot = ref<VChatStartupLog | null>(null)
|
const selectedJobInfoForViewSnapshot = ref<VChatStartupLog | null>(null)
|
||||||
|
|
||||||
function handleViewJobSnapshotButtonClick(record: VChatStartupLog) {
|
function handleViewJobSnapshotButtonClick(record: VChatStartupLog) {
|
||||||
|
gtagRenderer('view_job_snapshot_button_clicked')
|
||||||
selectedJobInfoForViewSnapshot.value = record
|
selectedJobInfoForViewSnapshot.value = record
|
||||||
drawVisibleModelValue.value = true
|
drawVisibleModelValue.value = true
|
||||||
}
|
}
|
||||||
async function handleViewJobOnlineButtonClick(encryptJobId: string) {
|
async function handleViewJobOnlineButtonClick(encryptJobId: string) {
|
||||||
|
gtagRenderer('view_job_online_button_clicked')
|
||||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||||
url: `https://www.zhipin.com/job_detail/${encryptJobId}.html`
|
url: `https://www.zhipin.com/job_detail/${encryptJobId}.html`
|
||||||
})
|
})
|
||||||
@@ -173,6 +206,7 @@ const historyDialogVisibleModelValue = ref(false)
|
|||||||
const selectedJobInfoForViewHistory = ref<VChatStartupLog | null>(null)
|
const selectedJobInfoForViewHistory = ref<VChatStartupLog | null>(null)
|
||||||
const selectedJobHistory = ref<null | JobInfoChangeLog[]>(null)
|
const selectedJobHistory = ref<null | JobInfoChangeLog[]>(null)
|
||||||
async function handleViewJobHistoryButtonClick(record: VChatStartupLog) {
|
async function handleViewJobHistoryButtonClick(record: VChatStartupLog) {
|
||||||
|
gtagRenderer('view_job_history_button_clicked')
|
||||||
let { data: historyList } = await electron.ipcRenderer.invoke(
|
let { data: historyList } = await electron.ipcRenderer.invoke(
|
||||||
'get-job-history-by-encrypt-id',
|
'get-job-history-by-encrypt-id',
|
||||||
record.encryptJobId
|
record.encryptJobId
|
||||||
@@ -227,12 +261,13 @@ async function handleViewJobHistoryButtonClick(record: VChatStartupLog) {
|
|||||||
// })
|
// })
|
||||||
|
|
||||||
if (!historyList.length) {
|
if (!historyList.length) {
|
||||||
|
gtagRenderer('job_history_is_not_found')
|
||||||
ElMessage.warning({
|
ElMessage.warning({
|
||||||
message: '未找到与此条目相关的历史变更记录,再多投一投吧'
|
message: '未找到与此条目相关的历史变更记录,再多投一投吧'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
gtagRenderer('job_history_is_found')
|
||||||
historyDialogVisibleModelValue.value = true
|
historyDialogVisibleModelValue.value = true
|
||||||
selectedJobInfoForViewHistory.value = record
|
selectedJobInfoForViewHistory.value = record
|
||||||
selectedJobHistory.value = historyList
|
selectedJobHistory.value = historyList
|
||||||
+32
-2
@@ -72,7 +72,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||||
<div class="w100px">
|
<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
|
>刷新</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +101,13 @@
|
|||||||
<JobInfoSnapshot
|
<JobInfoSnapshot
|
||||||
v-if="selectedJobInfoForViewSnapshot"
|
v-if="selectedJobInfoForViewSnapshot"
|
||||||
:job-info="selectedJobInfoForViewSnapshot"
|
:job-info="selectedJobInfoForViewSnapshot"
|
||||||
@closed="selectedJobInfoForViewSnapshot = null"
|
scene="markAsNotSuitRecord"
|
||||||
|
@closed="
|
||||||
|
() => {
|
||||||
|
gtagRenderer('mansr_closed')
|
||||||
|
selectedJobInfoForViewSnapshot = null
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</ElDrawer>
|
</ElDrawer>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,6 +121,7 @@ import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
|||||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/src/enums'
|
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/src/enums'
|
||||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const tableData = ref<VMarkAsNotSuitLog[]>([])
|
const tableData = ref<VMarkAsNotSuitLog[]>([])
|
||||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||||
@@ -122,6 +137,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
|||||||
const isTableLoading = ref(false)
|
const isTableLoading = ref(false)
|
||||||
async function getMarkAsNotSuitRecord() {
|
async function getMarkAsNotSuitRecord() {
|
||||||
try {
|
try {
|
||||||
|
gtagRenderer('mansr_request_sent', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
isTableLoading.value = true
|
isTableLoading.value = true
|
||||||
const { data: res } = (await electron.ipcRenderer.invoke('get-mark-as-not-suit-record', {
|
const { data: res } = (await electron.ipcRenderer.invoke('get-mark-as-not-suit-record', {
|
||||||
pageNo: pagination.value.pageNo,
|
pageNo: pagination.value.pageNo,
|
||||||
@@ -133,7 +152,16 @@ async function getMarkAsNotSuitRecord() {
|
|||||||
pageNo: res.pageNo,
|
pageNo: res.pageNo,
|
||||||
pageSize: pagination.value.pageSize
|
pageSize: pagination.value.pageSize
|
||||||
}
|
}
|
||||||
|
gtagRenderer('mansr_request_success', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
gtagRenderer('mansr_request_error', {
|
||||||
|
err,
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
console.log(err)
|
console.log(err)
|
||||||
tableData.value = []
|
tableData.value = []
|
||||||
} finally {
|
} finally {
|
||||||
@@ -158,6 +186,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function handleViewJobOnlineButtonClick(encryptJobId: string) {
|
async function handleViewJobOnlineButtonClick(encryptJobId: string) {
|
||||||
|
gtagRenderer('view_job_online_button_clicked')
|
||||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||||
url: `https://www.zhipin.com/job_detail/${encryptJobId}.html`
|
url: `https://www.zhipin.com/job_detail/${encryptJobId}.html`
|
||||||
})
|
})
|
||||||
@@ -167,6 +196,7 @@ const drawVisibleModelValue = ref(false)
|
|||||||
const selectedJobInfoForViewSnapshot = ref<VMarkAsNotSuitLog | null>(null)
|
const selectedJobInfoForViewSnapshot = ref<VMarkAsNotSuitLog | null>(null)
|
||||||
|
|
||||||
function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) {
|
function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) {
|
||||||
|
gtagRenderer('view_job_snapshot_button_clicked')
|
||||||
selectedJobInfoForViewSnapshot.value = record
|
selectedJobInfoForViewSnapshot.value = record
|
||||||
drawVisibleModelValue.value = true
|
drawVisibleModelValue.value = true
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
<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 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'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const formContent = ref({
|
||||||
|
autoReminder: {
|
||||||
|
throttleIntervalMinutes: 10,
|
||||||
|
rechatLimitDay: 21,
|
||||||
|
rechatContentSource: 1,
|
||||||
|
recentMessageQuantityForLlm: 8,
|
||||||
|
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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')
|
||||||
|
} 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')
|
||||||
|
} 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() {
|
||||||
|
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 {
|
||||||
|
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>
|
||||||
+30
-2
@@ -55,7 +55,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||||
<div class="w100px">
|
<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
|
>刷新</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +84,13 @@
|
|||||||
<JobInfoSnapshot
|
<JobInfoSnapshot
|
||||||
v-if="selectedJobInfoForViewSnapshot"
|
v-if="selectedJobInfoForViewSnapshot"
|
||||||
:job-info="selectedJobInfoForViewSnapshot"
|
:job-info="selectedJobInfoForViewSnapshot"
|
||||||
@closed="selectedJobInfoForViewSnapshot = null"
|
scene="startChatRecord"
|
||||||
|
@closed="
|
||||||
|
() => {
|
||||||
|
gtagRenderer('start_chat_record_closed')
|
||||||
|
selectedJobInfoForViewSnapshot = null
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</ElDrawer>
|
</ElDrawer>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,6 +103,7 @@ import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VCha
|
|||||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const tableData = ref<VChatStartupLog[]>([])
|
const tableData = ref<VChatStartupLog[]>([])
|
||||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||||
@@ -104,6 +119,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
|||||||
const isTableLoading = ref(false)
|
const isTableLoading = ref(false)
|
||||||
async function getAutoStartChatRecord() {
|
async function getAutoStartChatRecord() {
|
||||||
try {
|
try {
|
||||||
|
gtagRenderer('start_chat_record_request_sent', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
isTableLoading.value = true
|
isTableLoading.value = true
|
||||||
const { data: res } = (await electron.ipcRenderer.invoke('get-auto-start-chat-record', {
|
const { data: res } = (await electron.ipcRenderer.invoke('get-auto-start-chat-record', {
|
||||||
pageNo: pagination.value.pageNo,
|
pageNo: pagination.value.pageNo,
|
||||||
@@ -115,7 +134,16 @@ async function getAutoStartChatRecord() {
|
|||||||
pageNo: res.pageNo,
|
pageNo: res.pageNo,
|
||||||
pageSize: pagination.value.pageSize
|
pageSize: pagination.value.pageSize
|
||||||
}
|
}
|
||||||
|
gtagRenderer('start_chat_record_request_success', {
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
gtagRenderer('start_chat_record_request_error', {
|
||||||
|
err,
|
||||||
|
page_no: pagination.value.pageNo,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
})
|
||||||
console.log(err)
|
console.log(err)
|
||||||
tableData.value = []
|
tableData.value = []
|
||||||
} finally {
|
} finally {
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<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>当前版本: {{ 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 { TopRight, QuestionFilled } from '@element-plus/icons-vue'
|
||||||
|
import useBuildInfo from '@renderer/hooks/useBuildInfo'
|
||||||
|
import { debounce } from 'lodash-es'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
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 }
|
||||||
|
)
|
||||||
|
</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-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 |
@@ -0,0 +1,256 @@
|
|||||||
|
<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 } from 'vue'
|
||||||
|
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
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)
|
||||||
|
|
||||||
|
const scrollElRef = ref(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
async function sendLlmGeneratedContent() {
|
||||||
|
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 `***`
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -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 { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||||
import BootstrapSplash from '@renderer/page/BootstrapSplash/index.vue'
|
import BootstrapSplash from '@renderer/page/BootstrapSplash/index.vue'
|
||||||
|
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
@@ -17,55 +18,76 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/configuration',
|
path: '/llmConfig',
|
||||||
component: () => import('@renderer/page/Configuration/index.vue'),
|
component: () => import('@renderer/page/LlmConfig/index.vue'),
|
||||||
redirect: '/configuration/GeekAutoStartChatWithBoss',
|
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: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'GeekAutoStartChatWithBoss',
|
path: 'GeekAutoStartChatWithBoss',
|
||||||
component: () => import('@renderer/page/Configuration/GeekAutoStartChatWithBoss.vue'),
|
component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'BOSS炸弹'
|
title: 'BOSS炸弹'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'ReadNoReplyReminder',
|
path: 'ReadNoReplyReminder',
|
||||||
component: () => import('@renderer/page/Configuration/ReadNoReplyReminder.vue'),
|
component: () => import('@renderer/page/MainLayout/ReadNoReplyReminder.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '已读不回提醒器'
|
title: '已读不回提醒器'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'StartChatRecord',
|
path: 'StartChatRecord',
|
||||||
component: () => import('@renderer/page/Configuration/StartChatRecord.vue'),
|
component: () => import('@renderer/page/MainLayout/StartChatRecord.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '开聊记录'
|
title: '开聊记录'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'MarkAsNotSuitRecord',
|
path: 'MarkAsNotSuitRecord',
|
||||||
component: () => import('@renderer/page/Configuration/MarkAsNotSuitRecord.vue'),
|
component: () => import('@renderer/page/MainLayout/MarkAsNotSuitRecord.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '标记不合适记录'
|
title: '标记不合适记录'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'JobLibrary',
|
path: 'JobLibrary',
|
||||||
component: () => import('@renderer/page/Configuration/JobLibrary.vue'),
|
component: () => import('@renderer/page/MainLayout/JobLibrary.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '职位库'
|
title: '职位库'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'BossLibrary',
|
path: 'BossLibrary',
|
||||||
component: () => import('@renderer/page/Configuration/BossLibrary.vue'),
|
component: () => import('@renderer/page/MainLayout/BossLibrary.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Boss库'
|
title: 'Boss库'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'CompanyLibrary',
|
path: 'CompanyLibrary',
|
||||||
component: () => import('@renderer/page/Configuration/CompanyLibrary.vue'),
|
component: () => import('@renderer/page/MainLayout/CompanyLibrary.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '公司库'
|
title: '公司库'
|
||||||
}
|
}
|
||||||
@@ -114,7 +136,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import('@renderer/page/BootstrapSplash/page/DownloadingDependencies.vue'),
|
component: () => import('@renderer/page/BootstrapSplash/page/DownloadingDependencies.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '正在下载核心组件'
|
title: '正在下载核心组件'
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -125,12 +147,16 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
router.afterEach((to) => {
|
router.afterEach((to, from) => {
|
||||||
if (to.meta?.title) {
|
if (to.meta?.title) {
|
||||||
document.title = `${to.meta.title} - GeekGeekRun`
|
document.title = `${to.meta.title} - GeekGeekRun 牛人快跑`
|
||||||
} else {
|
} else {
|
||||||
document.title = `GeekGeekRun`
|
document.title = `GeekGeekRun 牛人快跑`
|
||||||
}
|
}
|
||||||
|
gtagRenderer('router_path_changed', {
|
||||||
|
from_path: from.fullPath,
|
||||||
|
to_path: to.fullPath
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export function gtagRenderer(name, params: any = null) {
|
||||||
|
try {
|
||||||
|
electron.ipcRenderer.send('gtag', {
|
||||||
|
name,
|
||||||
|
params: {
|
||||||
|
...(params ?? {}),
|
||||||
|
page_location: location.href,
|
||||||
|
page_title: document.title,
|
||||||
|
screen_w: window.screen?.width ?? null,
|
||||||
|
screen_h: window.screen?.height ?? null,
|
||||||
|
screen_dpr: window.devicePixelRatio
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log('gtag error', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "utils",
|
"description": "utils",
|
||||||
"scripts": {
|
"scripts": {},
|
||||||
},
|
|
||||||
"author": "geekgeekrun",
|
"author": "geekgeekrun",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {}
|
"dependencies": {
|
||||||
|
"openai": "^4.91.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+79
-9
@@ -141,6 +141,9 @@ importers:
|
|||||||
puppeteer-extra-plugin-stealth:
|
puppeteer-extra-plugin-stealth:
|
||||||
specifier: 2.11.2
|
specifier: 2.11.2
|
||||||
version: 2.11.2(puppeteer-extra@3.3.6)
|
version: 2.11.2(puppeteer-extra@3.3.6)
|
||||||
|
uuid:
|
||||||
|
specifier: ^11.1.0
|
||||||
|
version: 11.1.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@electron-toolkit/eslint-config':
|
'@electron-toolkit/eslint-config':
|
||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
@@ -226,6 +229,9 @@ importers:
|
|||||||
sass:
|
sass:
|
||||||
specifier: ^1.70.0
|
specifier: ^1.70.0
|
||||||
version: 1.70.0
|
version: 1.70.0
|
||||||
|
spark-md5:
|
||||||
|
specifier: ^3.0.2
|
||||||
|
version: 3.0.2
|
||||||
terser:
|
terser:
|
||||||
specifier: ^5.37.0
|
specifier: ^5.37.0
|
||||||
version: 5.37.0
|
version: 5.37.0
|
||||||
@@ -257,7 +263,11 @@ importers:
|
|||||||
specifier: ^1.8.27
|
specifier: ^1.8.27
|
||||||
version: 1.8.27(typescript@5.3.3)
|
version: 1.8.27(typescript@5.3.3)
|
||||||
|
|
||||||
packages/utils: {}
|
packages/utils:
|
||||||
|
dependencies:
|
||||||
|
openai:
|
||||||
|
specifier: ^4.91.1
|
||||||
|
version: 4.91.1
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1679,6 +1689,13 @@ packages:
|
|||||||
/@types/ms@0.7.34:
|
/@types/ms@0.7.34:
|
||||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
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:
|
/@types/node@18.19.15:
|
||||||
resolution: {integrity: sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==}
|
resolution: {integrity: sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2302,6 +2319,13 @@ packages:
|
|||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
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):
|
/acorn-jsx@5.3.2(acorn@8.11.3):
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2349,7 +2373,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
humanize-ms: 1.2.1
|
humanize-ms: 1.2.1
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
|
||||||
|
|
||||||
/aggregate-error@3.1.0:
|
/aggregate-error@3.1.0:
|
||||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||||
@@ -2546,7 +2569,6 @@ packages:
|
|||||||
|
|
||||||
/asynckit@0.4.0:
|
/asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/at-least-node@1.0.0:
|
/at-least-node@1.0.0:
|
||||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||||
@@ -2904,7 +2926,6 @@ packages:
|
|||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/commander@2.20.3:
|
/commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
@@ -3108,7 +3129,6 @@ packages:
|
|||||||
/delayed-stream@1.0.0:
|
/delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/delegates@1.0.0:
|
/delegates@1.0.0:
|
||||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||||
@@ -3607,6 +3627,11 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
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:
|
/execa@5.1.1:
|
||||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3776,6 +3801,10 @@ packages:
|
|||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/form-data-encoder@1.7.2:
|
||||||
|
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/form-data@4.0.0:
|
/form-data@4.0.0:
|
||||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -3783,7 +3812,14 @@ packages:
|
|||||||
asynckit: 0.4.0
|
asynckit: 0.4.0
|
||||||
combined-stream: 1.0.8
|
combined-stream: 1.0.8
|
||||||
mime-types: 2.1.35
|
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:
|
/formdata-polyfill@4.0.10:
|
||||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||||
@@ -4162,7 +4198,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
|
||||||
|
|
||||||
/iconv-corefoundation@1.1.7:
|
/iconv-corefoundation@1.1.7:
|
||||||
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
||||||
@@ -4643,14 +4678,12 @@ packages:
|
|||||||
/mime-db@1.52.0:
|
/mime-db@1.52.0:
|
||||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mime-types@2.1.35:
|
/mime-types@2.1.35:
|
||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.52.0
|
mime-db: 1.52.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mime@2.6.0:
|
/mime@2.6.0:
|
||||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||||
@@ -5002,6 +5035,29 @@ packages:
|
|||||||
mimic-fn: 2.1.0
|
mimic-fn: 2.1.0
|
||||||
dev: true
|
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:
|
/optionator@0.9.3:
|
||||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -5726,6 +5782,10 @@ packages:
|
|||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/spark-md5@3.0.2:
|
||||||
|
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sprintf-js@1.1.3:
|
/sprintf-js@1.1.3:
|
||||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@@ -6360,6 +6420,11 @@ packages:
|
|||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
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:
|
/uuid@8.3.2:
|
||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -6499,6 +6564,11 @@ packages:
|
|||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
dev: false
|
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:
|
/webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
Reference in New Issue
Block a user