mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-05-11 18:09:50 +08:00
Merge branch 'feature/auto-reminder-gpt' into feature/ui
This commit is contained in:
@@ -9,6 +9,10 @@
|
||||
"expectJobRegExpStr": "",
|
||||
"autoReminder": {
|
||||
"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": {}
|
||||
}]
|
||||
@@ -1,4 +1,4 @@
|
||||
[
|
||||
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技",
|
||||
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","京东","沃东天骏","达达","百度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","理想","蔚来","顺丰","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","蚂蚁","飞猪","高德","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","得物","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","首汽租车","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技"
|
||||
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
|
||||
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","京东","沃东天骏","达达","达冠","百度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","理想","蔚来","顺丰","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","蚂蚁","高德","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 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 recommendJobPageUrl = `https://www.zhipin.com/web/geek/job-recommend`
|
||||
@@ -191,22 +206,32 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
|
||||
return result
|
||||
}
|
||||
|
||||
export function testIfJobTitleOrDescriptionSuit (jobInfo, regExpStr) {
|
||||
if (!regExpStr) {
|
||||
return true
|
||||
}
|
||||
export function testIfJobTitleOrDescriptionSuit (jobInfo) {
|
||||
let isJobNameSuit = true
|
||||
try {
|
||||
const regExp = new RegExp(regExpStr, 'i')
|
||||
if (
|
||||
!regExp.test(jobInfo.jobName)
|
||||
&& !regExp.test(jobInfo.positionName)
|
||||
&& !regExp.test(jobInfo.postDescription)
|
||||
) {
|
||||
return false
|
||||
if (expectJobNameRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobNameRegExpStr, 'i')
|
||||
isJobNameSuit = regExp.test(jobInfo.jobName)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return true
|
||||
let isJobTypeSuit = true
|
||||
try {
|
||||
if (expectJobTypeRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobTypeRegExpStr, 'i')
|
||||
isJobTypeSuit = regExp.test(jobInfo.positionName)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
let isJobDescSuit = true
|
||||
try {
|
||||
if (expectJobDescRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobDescRegExpStr, 'i')
|
||||
isJobDescSuit = regExp.test(jobInfo.postDescription)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return isJobNameSuit && isJobTypeSuit && isJobDescSuit
|
||||
}
|
||||
|
||||
async function setFilterCondition (selectedFilters) {
|
||||
@@ -239,7 +264,7 @@ async function setFilterCondition (selectedFilters) {
|
||||
const placeholderText = placeholderTexts[i]
|
||||
const filterDropdownProxy = await (async () => {
|
||||
const jsHandle = (await page.evaluateHandle((placeholderText) => {
|
||||
const filterBar = document.querySelector('.job-recommend-main .job-recommend-search')
|
||||
const filterBar = document.querySelector('.page-jobs-main .filter-condition-inner')
|
||||
const dropdownEntry = filterBar.__vue__.$children.find(it => it.placeholder === placeholderText)
|
||||
return dropdownEntry.$el
|
||||
}, placeholderText)).asElement();
|
||||
@@ -264,18 +289,18 @@ async function setFilterCondition (selectedFilters) {
|
||||
const optionKaPrefix = optionKaPrefixes[i]
|
||||
if (!currentFilterConditions.length) {
|
||||
if (placeholderText === '公司行业') {
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.job-recommend-main .recommend-search-more .active[ka^="${optionKaPrefix}"]`)
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||||
for (const it of activeOptionElAtCurrentFilterProxyList) {
|
||||
await it.click()
|
||||
}
|
||||
} else {
|
||||
// select 不限 immediately
|
||||
const buxianOptionElProxy = await page.$(`.job-recommend-main .recommend-search-more [ka="${optionKaPrefix}${0}"]`)
|
||||
const buxianOptionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${0}"]`)
|
||||
await buxianOptionElProxy.click()
|
||||
}
|
||||
} else {
|
||||
//#region uncheck options perviously checked but not existed in current filter.
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.job-recommend-main .recommend-search-more .active[ka^="${optionKaPrefix}"]`)
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||||
const activeOptionValues = (await Promise.all(
|
||||
activeOptionElAtCurrentFilterProxyList.map(elProxy => {
|
||||
return elProxy.evaluate((el) => {
|
||||
@@ -314,7 +339,7 @@ async function setFilterCondition (selectedFilters) {
|
||||
optionValue = conditionToCheck[j]
|
||||
}
|
||||
await sleepWithRandomDelay(500)
|
||||
const optionElProxy = await page.$(`.job-recommend-main .recommend-search-more [ka="${optionKaPrefix}${optionValue}"]`)
|
||||
const optionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${optionValue}"]`)
|
||||
if (!optionElProxy) {
|
||||
continue;
|
||||
}
|
||||
@@ -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
|
||||
afterPageLoad: while (true) {
|
||||
let expectJobList
|
||||
@@ -397,20 +422,21 @@ async function toRecommendPage (hooks) {
|
||||
await sleepWithRandomDelay(2500)
|
||||
|
||||
await Promise.all([
|
||||
page.waitForSelector('.job-recommend-main .recommend-search-expect .recommend-job-btn'),
|
||||
page.waitForSelector('.c-expect-select .expect-list .expect-item'),
|
||||
page.waitForSelector('.job-list-container .rec-job-list')
|
||||
])
|
||||
await page.click(`.c-expect-select .expect-list .expect-item`)
|
||||
const currentActiveJobIndex = await page.evaluate(`
|
||||
[...document.querySelectorAll('.job-recommend-main .recommend-search-expect .recommend-job-btn')].findIndex(it => it.classList.contains('active'))
|
||||
[...document.querySelectorAll('.c-expect-select .expect-list .expect-item')].findIndex(it => it.classList.contains('active'))
|
||||
`)
|
||||
|
||||
expectJobList = await page.evaluate(`document.querySelector('.job-recommend-search')?.__vue__?.expectList`)
|
||||
expectJobList = await page.evaluate(`document.querySelector('.c-expect-select')?.__vue__?.expectList`)
|
||||
if (currentActiveJobIndex === currentExceptJobIndex) {
|
||||
// first navigation and can immediately start chat (recommend job)
|
||||
} else {
|
||||
// not first navigation and should choose a job (except job)
|
||||
// click first expect job
|
||||
const expectJobTabHandlers = await page.$$('.job-recommend-main .recommend-search-expect .recommend-job-btn')
|
||||
const expectJobTabHandlers = await page.$$('.c-expect-select .expect-list .expect-item')
|
||||
await expectJobTabHandlers[currentExceptJobIndex].click()
|
||||
await page.waitForResponse(
|
||||
response => {
|
||||
@@ -461,7 +487,7 @@ async function toRecommendPage (hooks) {
|
||||
|
||||
// job list
|
||||
const recommendJobListElProxy = await page.$('.job-list-container .rec-job-list')
|
||||
let jobListData = await page.evaluate(`document.querySelector('.job-recommend-main')?.__vue__?.jobList`)
|
||||
let jobListData = await page.evaluate(`document.querySelector('.page-jobs-main')?.__vue__?.jobList`)
|
||||
let hasReachLastPage = false
|
||||
let targetJobIndex = -1
|
||||
let targetJobData
|
||||
@@ -494,10 +520,10 @@ async function toRecommendPage (hooks) {
|
||||
) {
|
||||
scrolledHeight += increase
|
||||
await page.mouse.wheel({deltaY: increase});
|
||||
await sleep(1)
|
||||
await sleep(100)
|
||||
await requestNextPagePromiseWithResolver?.promise
|
||||
hasReachLastPage = await page.evaluate(`
|
||||
!(document.querySelector('.job-recommend-main')?.__vue__?.hasMore)
|
||||
!(document.querySelector('.page-jobs-main')?.__vue__?.hasMore)
|
||||
`)
|
||||
if (hasReachLastPage) {
|
||||
console.log(`Arrive the terminal of the job list.`)
|
||||
@@ -505,16 +531,16 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
requestNextPagePromiseWithResolver = null
|
||||
|
||||
await sleep(3000)
|
||||
await sleep(5000)
|
||||
jobListData = await page.evaluate(
|
||||
`
|
||||
document.querySelector('.job-recommend-main')?.__vue__?.jobList
|
||||
document.querySelector('.page-jobs-main')?.__vue__?.jobList
|
||||
`
|
||||
)
|
||||
tempTargetJobIndexToCheckDetail = jobListData.findIndex(it =>
|
||||
!blockBossNotNewChat.has(it.encryptBossId) &&
|
||||
!blockBossNotActive.has(it.encryptBossId) &&
|
||||
[...expectCompanySet].find(name => it.brandName.includes(name)) &&
|
||||
[...expectCompanySet].find(name => it.brandName?.toLowerCase?.()?.includes(name.toLowerCase())) &&
|
||||
!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 targetJobElProxy = recommendJobItemList[tempTargetJobIndexToCheckDetail]
|
||||
// click that element
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.scrollTop = 0
|
||||
})
|
||||
await sleep(500)
|
||||
await targetJobElProxy.click()
|
||||
await page.waitForResponse(
|
||||
response => {
|
||||
@@ -594,7 +624,7 @@ async function toRecommendPage (hooks) {
|
||||
continue continueFind
|
||||
}
|
||||
if (
|
||||
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo, expectJobRegExpStr)
|
||||
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo)
|
||||
) {
|
||||
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
|
||||
try {
|
||||
@@ -612,7 +642,6 @@ async function toRecommendPage (hooks) {
|
||||
)
|
||||
} catch {
|
||||
}
|
||||
debugger
|
||||
continue continueFind
|
||||
}
|
||||
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)
|
||||
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
|
||||
await startChatButtonProxy.click()
|
||||
|
||||
@@ -714,9 +747,11 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
// for of reach terminal
|
||||
if (
|
||||
currentExceptJobIndex + 1 > expectJobList.length
|
||||
currentExceptJobIndex + 1 >= expectJobList.length
|
||||
) {
|
||||
hooks.noPositionFoundForCurrentJob?.call()
|
||||
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
||||
await sleep((20 + 30 * Math.random()) * 1000)
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForNavigation()
|
||||
@@ -724,8 +759,7 @@ async function toRecommendPage (hooks) {
|
||||
currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
|
||||
} else {
|
||||
hooks.noPositionFoundForCurrentJob?.call()
|
||||
hooks.noPositionFoundAfterTraverseAllJob?.call()
|
||||
|
||||
await sleep((10 + 15 * Math.random()) * 1000)
|
||||
currentExceptJobIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,18 @@ import os from 'node:os'
|
||||
import defaultDingtalkConf from './default-config-file/dingtalk.json' assert {type: 'json'}
|
||||
import defaultBossConf from './default-config-file/boss.json' assert {type: 'json'}
|
||||
import defaultTargetCompanyListConf from './default-config-file/target-company-list.json' assert {type: 'json'}
|
||||
import defaultLlmConf from './default-config-file/llm.json' assert { type: 'json' }
|
||||
|
||||
import defaultBossCookieStorage from './default-storage-file/boss-cookies.json' assert { type: 'json' }
|
||||
import defaultBossLocalStorageStorage from './default-storage-file/boss-local-storage.json' assert { type: 'json' }
|
||||
import defaultJobNotSuitReasonCodeToTextCacheStorage from './default-storage-file/job-not-suit-reason-code-to-text-cache.json' assert { type: 'json' }
|
||||
export const configFileNameList = ['boss.json', 'dingtalk.json', 'target-company-list.json']
|
||||
export const configFileNameList = ['boss.json', 'dingtalk.json', 'target-company-list.json', 'llm.json']
|
||||
|
||||
const defaultConfigFileContentMap = {
|
||||
'boss.json': JSON.stringify(defaultBossConf),
|
||||
'dingtalk.json': JSON.stringify(defaultDingtalkConf),
|
||||
'target-company-list.json': JSON.stringify(defaultTargetCompanyListConf)
|
||||
'target-company-list.json': JSON.stringify(defaultTargetCompanyListConf),
|
||||
'llm.json': JSON.stringify(defaultLlmConf)
|
||||
}
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
@@ -69,8 +71,12 @@ export const readConfigFile = (fileName) => {
|
||||
)
|
||||
} catch {
|
||||
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
||||
ensureConfigFileExist()
|
||||
o = JSON.parse(defaultConfigFileContentMap[fileName])
|
||||
if (defaultConfigFileContentMap[fileName]) {
|
||||
ensureConfigFileExist()
|
||||
o = JSON.parse(defaultConfigFileContentMap[fileName])
|
||||
} else {
|
||||
o = null
|
||||
}
|
||||
}
|
||||
|
||||
return o
|
||||
@@ -112,7 +118,8 @@ export const ensureStorageFileExist = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const readStorageFile = (fileName) => {
|
||||
export const readStorageFile = (fileName, { isJson } = {}) => {
|
||||
isJson = isJson ?? true
|
||||
const joinedPath = path.join(storageFilePath, fileName)
|
||||
|
||||
if (!fs.existsSync(
|
||||
@@ -123,21 +130,36 @@ export const readStorageFile = (fileName) => {
|
||||
|
||||
let o
|
||||
try {
|
||||
o = JSON.parse(
|
||||
fs.readFileSync(joinedPath)
|
||||
)
|
||||
const content = fs.readFileSync(joinedPath)
|
||||
if (isJson) {
|
||||
o = JSON.parse(content)
|
||||
}
|
||||
else {
|
||||
o = content.toString()
|
||||
}
|
||||
} catch {
|
||||
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
||||
ensureStorageFileExist()
|
||||
o = JSON.parse(defaultStorageFileContentMap[fileName])
|
||||
if (isJson) {
|
||||
o = JSON.parse(defaultStorageFileContentMap[fileName] ?? 'null')
|
||||
}
|
||||
else {
|
||||
o = defaultStorageFileContentMap[fileName] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
export const writeStorageFile = async (fileName, content) => {
|
||||
export const writeStorageFile = async (fileName, content, { isJson } = {}) => {
|
||||
isJson = isJson ?? true
|
||||
const filePath = path.join(storageFilePath, fileName)
|
||||
const fileContent = JSON.stringify(content)
|
||||
let fileContent
|
||||
if (isJson) {
|
||||
fileContent = JSON.stringify(content)
|
||||
} else {
|
||||
fileContent = content
|
||||
}
|
||||
return fsPromise.writeFile(
|
||||
filePath,
|
||||
fileContent
|
||||
|
||||
Binary file not shown.
@@ -6,68 +6,28 @@ import {
|
||||
sleep,
|
||||
sleepWithRandomDelay
|
||||
} from '@geekgeekrun/utils/sleep.mjs'
|
||||
import extractZip from 'extract-zip'
|
||||
import { blockNavigation } from '@geekgeekrun/utils/puppeteer/block-navigation.mjs'
|
||||
import {
|
||||
writeStorageFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path';
|
||||
import JSON5 from 'json5'
|
||||
import url from 'url';
|
||||
import packageJson from './package.json' assert {type: 'json'}
|
||||
import {
|
||||
runtimeFolderPath,
|
||||
ensureEditThisCookie,
|
||||
editThisCookieExtensionPath,
|
||||
isRunFromUi,
|
||||
} from './utils.mjs'
|
||||
|
||||
import { EventEmitter } from 'node:events'
|
||||
|
||||
export const loginEventBus = new EventEmitter()
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
const isUiDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
const extensionDir = path.join(
|
||||
runtimeFolderPath,
|
||||
'chrome-extensions'
|
||||
)
|
||||
if (!fs.existsSync(
|
||||
runtimeFolderPath
|
||||
)) {
|
||||
fs.mkdirSync(runtimeFolderPath)
|
||||
}
|
||||
if (!fs.existsSync(extensionDir)) {
|
||||
fs.mkdirSync(extensionDir)
|
||||
}
|
||||
const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
|
||||
|
||||
let editThisCookieZipPath
|
||||
async function getEditThisCookieZipPath () {
|
||||
if (editThisCookieZipPath) {
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
if (isRunFromUi) {
|
||||
const { app } = await import('electron')
|
||||
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
|
||||
} else {
|
||||
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
|
||||
}
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
if (!fs.existsSync(
|
||||
path.join(editThisCookieExtensionPath, 'manifest.json')
|
||||
)) {
|
||||
await extractZip(
|
||||
await getEditThisCookieZipPath(),
|
||||
{
|
||||
dir: extensionDir
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
await ensureEditThisCookie()
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import extractZip from 'extract-zip'
|
||||
import packageJson from './package.json' assert {type: 'json'}
|
||||
|
||||
export const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
const isUiDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
export const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
|
||||
const extensionDir = path.join(
|
||||
runtimeFolderPath,
|
||||
'chrome-extensions'
|
||||
)
|
||||
if (!fs.existsSync(
|
||||
runtimeFolderPath
|
||||
)) {
|
||||
fs.mkdirSync(runtimeFolderPath)
|
||||
}
|
||||
if (!fs.existsSync(extensionDir)) {
|
||||
fs.mkdirSync(extensionDir)
|
||||
}
|
||||
export const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
|
||||
|
||||
let editThisCookieZipPath
|
||||
async function getEditThisCookieZipPath () {
|
||||
if (editThisCookieZipPath) {
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
if (isRunFromUi) {
|
||||
const { app } = await import('electron')
|
||||
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
|
||||
} else {
|
||||
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
|
||||
}
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
|
||||
export async function ensureEditThisCookie () {
|
||||
let isNeedExtractEditThisCookie = false
|
||||
const manifestFilePath = path.join(editThisCookieExtensionPath, 'manifest.json')
|
||||
if (!fs.existsSync(
|
||||
manifestFilePath
|
||||
)) {
|
||||
isNeedExtractEditThisCookie = true
|
||||
} else {
|
||||
let manifest
|
||||
try {
|
||||
manifest = JSON.parse(fs.readFileSync(manifestFilePath, { encoding: 'utf-8' }))
|
||||
if (!manifest.manifest_version || manifest.manifest_version <= 2) {
|
||||
isNeedExtractEditThisCookie = true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.log(`未能获取到文件内容`)
|
||||
isNeedExtractEditThisCookie = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isNeedExtractEditThisCookie) {
|
||||
if (
|
||||
fs.existsSync(
|
||||
editThisCookieExtensionPath
|
||||
)
|
||||
) {
|
||||
fs.rmSync(
|
||||
editThisCookieExtensionPath,
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
)
|
||||
}
|
||||
await extractZip(
|
||||
await getEditThisCookieZipPath(),
|
||||
{
|
||||
dir: extensionDir
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export class ChatMessageRecord {
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
style?: 'sent' | 'receive';
|
||||
style?: 'sent' | 'received';
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
|
||||
64
packages/sqlite-plugin/src/entity/LlmModelUsageRecord.ts
Normal file
64
packages/sqlite-plugin/src/entity/LlmModelUsageRecord.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, PrimaryGeneratedColumn, Column, Index } = requireTypeorm()
|
||||
|
||||
@Entity()
|
||||
@Index(["providerCompleteApiUrl", "model", "providerApiSecret"])
|
||||
export class LlmModelUsageRecord {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
providerCompleteApiUrl: string
|
||||
|
||||
@Column()
|
||||
model: string
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
providerApiSecret: string
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
completionTokens?: number;
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
promptTokens?: number;
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
promptCacheHitTokens?: number
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
promptCacheMissTokens?: number
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
totalTokens?: number;
|
||||
|
||||
@Column()
|
||||
requestStartTime: Date
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
requestEndTime?: Date
|
||||
|
||||
@Column()
|
||||
hasError: boolean
|
||||
|
||||
@Column()
|
||||
errorMessage: string
|
||||
|
||||
@Column({
|
||||
nullable: true
|
||||
})
|
||||
requestScene?: number
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { CompanyInfoChangeLog } from "./entity/CompanyInfoChangeLog";
|
||||
import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
|
||||
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
|
||||
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
|
||||
import { LlmModelUsageRecord } from "./entity/LlmModelUsageRecord";
|
||||
|
||||
function getBossInfoIfIsEqual (savedOne, currentOne) {
|
||||
if (savedOne === currentOne) {
|
||||
@@ -307,3 +308,21 @@ export async function saveChatMessageRecord(
|
||||
//#endregion
|
||||
return
|
||||
}
|
||||
|
||||
export async function saveGptCompletionRequestRecord(
|
||||
ds: DataSource,
|
||||
records: LlmModelUsageRecord[]
|
||||
) {
|
||||
//#region mark-as-not-suit-log
|
||||
const list = records.map(it => {
|
||||
const o = new LlmModelUsageRecord()
|
||||
for (const k of Object.keys(it)) {
|
||||
o[k] = it[k]
|
||||
}
|
||||
return o
|
||||
})
|
||||
const chatMessageRecordRepository = ds.getRepository(LlmModelUsageRecord);
|
||||
await chatMessageRecordRepository.save(list);
|
||||
//#endregion
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { VJobLibrary } from "./entity/VJobLibrary";
|
||||
import { VCompanyLibrary } from "./entity/VCompanyLibrary"
|
||||
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
|
||||
import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
||||
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
||||
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord } from "./handlers";
|
||||
@@ -53,10 +54,11 @@ export function initDb(dbFilePath) {
|
||||
MarkAsNotSuitLog,
|
||||
VMarkAsNotSuitLog,
|
||||
ChatMessageRecord,
|
||||
LlmModelUsageRecord,
|
||||
],
|
||||
migrations: [
|
||||
UpdateChatStartupLogTable1729182577167,
|
||||
UpdateBossInfoTable1732032381304
|
||||
UpdateBossInfoTable1732032381304,
|
||||
],
|
||||
migrationsRun: true
|
||||
});
|
||||
|
||||
@@ -5,31 +5,28 @@ const viewNames = [
|
||||
"v_chat_startup_log",
|
||||
"v_company_library",
|
||||
"v_job_library",
|
||||
"v_mark_as_not_suit_log"
|
||||
"v_mark_as_not_suit_log",
|
||||
];
|
||||
|
||||
export class UpdateBossInfoTable1732032381304 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const viewName of viewNames) {
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
||||
}
|
||||
if (await queryRunner.hasTable("boss_info")) {
|
||||
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) {
|
||||
await queryRunner.changeColumn(
|
||||
'boss_info',
|
||||
'encryptCompanyId',
|
||||
new TableColumn({
|
||||
name: 'encryptCompanyId',
|
||||
type: 'varchar',
|
||||
isNullable: true
|
||||
})
|
||||
)
|
||||
debugger
|
||||
}
|
||||
}
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const viewName of viewNames) {
|
||||
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
if (await queryRunner.hasTable("boss_info")) {
|
||||
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) {
|
||||
await queryRunner.changeColumn(
|
||||
"boss_info",
|
||||
"encryptCompanyId",
|
||||
new TableColumn({
|
||||
name: "encryptCompanyId",
|
||||
type: "varchar",
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"minimist": "^1.2.8",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2"
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
@@ -72,6 +73,7 @@
|
||||
"normalize.css": "^8.0.1",
|
||||
"prettier": "^3.2.4",
|
||||
"sass": "^1.70.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "^5.3.3",
|
||||
"unocss": "^0.58.5",
|
||||
|
||||
1
packages/ui/src/common/constant.ts
Normal file
1
packages/ui/src/common/constant.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
|
||||
@@ -8,3 +8,13 @@ export enum AUTO_CHAT_ERROR_EXIT_CODE {
|
||||
AUTO_START_CHAT_DAEMON_PROCESS_SUICIDE = 86,
|
||||
AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87,
|
||||
}
|
||||
|
||||
export enum RECHAT_CONTENT_SOURCE {
|
||||
LOOK_FORWARD_EMOTION = 1,
|
||||
GEMINI_WITH_CHAT_CONTEXT = 2
|
||||
}
|
||||
|
||||
export enum RECHAT_LLM_FALLBACK {
|
||||
SEND_LOOK_FORWARD_EMOTION = 1,
|
||||
EXIT_REMINDER_PROGRAM = 2
|
||||
}
|
||||
|
||||
91
packages/ui/src/common/utils/resume.ts
Normal file
91
packages/ui/src/common/utils/resume.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export interface ResumeContent {
|
||||
name: string
|
||||
workYearDesc: string
|
||||
expectJob: string
|
||||
userDescription: string
|
||||
geekWorkExpList: Array<{
|
||||
company: string
|
||||
positionName: string
|
||||
startYearMon: string | null
|
||||
endYearMon: string | null
|
||||
performance: string
|
||||
workDescription: string
|
||||
}>
|
||||
geekProjExpList: Array<{
|
||||
name: string
|
||||
startYearMon: string
|
||||
endYearMon: string
|
||||
roleName: string
|
||||
projectDescription: string
|
||||
performance: string
|
||||
}>
|
||||
expectSalary: [string, string]
|
||||
}
|
||||
|
||||
export function formatResumeJsonToMarkdown(resume) {
|
||||
const basicInfoText = [
|
||||
['# 姓名', resume.content.name],
|
||||
['# 工作年限', resume.content.workYearDesc],
|
||||
['# 期望职位', resume.content.expectJob],
|
||||
['# 个人优势', resume.content.userDescription]
|
||||
]
|
||||
.filter((it) => {
|
||||
return Boolean(it[1]?.trim())
|
||||
})
|
||||
.map((it) => it.join('\n'))
|
||||
.join('\n\n')
|
||||
|
||||
let formattedWorkExpText = resume.content.geekWorkExpList
|
||||
.filter((it) => Boolean(it.company?.trim()))
|
||||
.map((it) => {
|
||||
const info = [
|
||||
[`职务`, it.positionName],
|
||||
[`任职时间`],
|
||||
[`工作描述`, it.workDescription],
|
||||
[`工作业绩`, it.performance]
|
||||
].filter((it) => {
|
||||
return Boolean(it[1]?.trim())
|
||||
})
|
||||
return [[`## ${it.company}`], ...info].map((it) => it.join('\n')).join('\n\n')
|
||||
})
|
||||
.join('\n')
|
||||
if (formattedWorkExpText?.trim()) {
|
||||
formattedWorkExpText = '# 工作经历\n' + formattedWorkExpText
|
||||
}
|
||||
|
||||
let formattedProjWorkExpText = resume.content.geekProjExpList
|
||||
.filter((it) => Boolean(it.name?.trim()))
|
||||
.map((it) => {
|
||||
const info = [
|
||||
[`## ${it.name}`],
|
||||
[`项目角色`, it.roleName],
|
||||
[`项目时间`],
|
||||
[`工作描述`, it.projectDescription],
|
||||
[`工作业绩`, it.performance]
|
||||
].filter((it) => {
|
||||
return Boolean(it[1]?.trim())
|
||||
})
|
||||
|
||||
return [[`## ${it.name}`], ...info].map((it) => it.join('\n')).join('\n\n')
|
||||
})
|
||||
.join('\n')
|
||||
if (formattedProjWorkExpText?.trim()) {
|
||||
formattedProjWorkExpText = '# 项目经历\n' + formattedProjWorkExpText
|
||||
}
|
||||
|
||||
const result = `${basicInfoText}\n\n${formattedWorkExpText}\n\n${formattedProjWorkExpText}`
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function checkIsResumeContentValid(resumeItem: { content: ResumeContent }) {
|
||||
return (
|
||||
!!resumeItem?.content &&
|
||||
resumeItem.content.geekProjExpList?.[0]?.name?.trim() &&
|
||||
resumeItem.content.geekWorkExpList?.[0]?.positionName?.trim()
|
||||
)
|
||||
}
|
||||
|
||||
export function resumeContentEnoughDetect(resumeItem: { content: ResumeContent }) {
|
||||
return resumeItem?.content && formatResumeJsonToMarkdown(resumeItem)?.length > 800
|
||||
}
|
||||
22
packages/ui/src/main/features/llm-request-log.ts
Normal file
22
packages/ui/src/main/features/llm-request-log.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { saveGptCompletionRequestRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
|
||||
export enum RequestSceneEnum {
|
||||
testing = 1,
|
||||
readNoReplyAutoReminder = 2,
|
||||
geekAutoStartChatWithBoss = 3
|
||||
}
|
||||
|
||||
let dbInitPromise
|
||||
export const recordGptCompletionRequest = async (payload) => {
|
||||
const { getPublicDbFilePath } = await import(
|
||||
'@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
)
|
||||
const { initDb } = await import('@geekgeekrun/sqlite-plugin')
|
||||
|
||||
if (!dbInitPromise) {
|
||||
dbInitPromise = initDb(getPublicDbFilePath())
|
||||
}
|
||||
const ds = await dbInitPromise
|
||||
const o = { ...payload }
|
||||
await saveGptCompletionRequestRecord(ds, [o])
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { app } from 'electron'
|
||||
import { initPuppeteer } from '@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
|
||||
import extractZip from 'extract-zip'
|
||||
import {
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
@@ -17,10 +16,6 @@ import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss
|
||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import url from 'url'
|
||||
import packageJson from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension/package.json' assert { type: 'json' }
|
||||
import { Target } from 'puppeteer'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
@@ -31,38 +26,11 @@ import { type ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/src/entity/Ch
|
||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
import {
|
||||
ensureEditThisCookie,
|
||||
editThisCookieExtensionPath
|
||||
} from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension/utils.mjs'
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
const extensionDir = path.join(runtimeFolderPath, 'chrome-extensions')
|
||||
if (!fs.existsSync(runtimeFolderPath)) {
|
||||
fs.mkdirSync(runtimeFolderPath)
|
||||
}
|
||||
if (!fs.existsSync(extensionDir)) {
|
||||
fs.mkdirSync(extensionDir)
|
||||
}
|
||||
const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
|
||||
|
||||
let editThisCookieZipPath
|
||||
async function getEditThisCookieZipPath() {
|
||||
if (editThisCookieZipPath) {
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
if (isRunFromUi) {
|
||||
const { app } = await import('electron')
|
||||
editThisCookieZipPath = path.join(
|
||||
app.getAppPath(),
|
||||
'./node_modules',
|
||||
packageJson.name,
|
||||
'extensions',
|
||||
'EditThisCookie.zip'
|
||||
)
|
||||
} else {
|
||||
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
|
||||
}
|
||||
return editThisCookieZipPath
|
||||
}
|
||||
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||
|
||||
const attachRequestsListener = async (target: Target) => {
|
||||
@@ -221,7 +189,7 @@ const attachRequestsListener = async (target: Target) => {
|
||||
mappedItem.mid = it.mid
|
||||
mappedItem.encryptFromUserId = it.isSelf ? currentUserInfo.encryptUserId : bossInfo.encryptBossId
|
||||
mappedItem.encryptToUserId = it.isSelf ? bossInfo.encryptBossId : currentUserInfo.encryptUserId
|
||||
mappedItem.style = it.style
|
||||
mappedItem.style = it.isSelf ? 'sent' : 'received'
|
||||
mappedItem.type = it.type
|
||||
mappedItem.time = it.time ? new Date(it.time) : null
|
||||
mappedItem.text = it.text
|
||||
@@ -247,12 +215,7 @@ const attachRequestsListener = async (target: Target) => {
|
||||
|
||||
export async function launchBossSite() {
|
||||
app.dock?.hide()
|
||||
if (!fs.existsSync(path.join(editThisCookieExtensionPath, 'manifest.json'))) {
|
||||
await extractZip(await getEditThisCookieZipPath(), {
|
||||
dir: extensionDir
|
||||
})
|
||||
}
|
||||
|
||||
await ensureEditThisCookie()
|
||||
const bossCookies = readStorageFile('boss-cookies.json')
|
||||
const bossLocalStorage = readStorageFile('boss-local-storage.json')
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createMainWindow } from '../../window/mainWindow'
|
||||
import './app-menu'
|
||||
import initIpc from './ipc'
|
||||
import gtag from '../../utils/gtag'
|
||||
import initPublicIpc from '../../utils/initPublicIpc'
|
||||
export function openSettingWindow() {
|
||||
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
@@ -31,6 +32,7 @@ export function openSettingWindow() {
|
||||
|
||||
// IPC test
|
||||
ipcMain.on('ping', () => console.log('pong'))
|
||||
initPublicIpc()
|
||||
initIpc()
|
||||
|
||||
app.on('activate', function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ipcMain, shell, app } from 'electron'
|
||||
|
||||
import path from 'path'
|
||||
import * as childProcess from 'node:child_process'
|
||||
import {
|
||||
ensureConfigFileExist,
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
readConfigFile,
|
||||
writeConfigFile,
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
writeStorageFile,
|
||||
storageFilePath
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { ChildProcess } from 'child_process'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
@@ -30,14 +31,27 @@ import { pipeWriteRegardlessError } from '../../utils/pipe'
|
||||
import { WriteStream } from 'node:fs'
|
||||
// eslint-disable-next-line vue/prefer-import-from-vue
|
||||
import { hasOwn } from '@vue/shared'
|
||||
import { createLlmConfigWindow, llmConfigWindow } from '../../../window/llmConfigWindow'
|
||||
import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/resumeEditorWindow'
|
||||
import {
|
||||
getValidTemplate,
|
||||
requestNewMessageContent
|
||||
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
|
||||
import {
|
||||
autoReminderPromptTemplateFileName,
|
||||
writeDefaultAutoRemindPrompt
|
||||
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
|
||||
import {
|
||||
checkIsResumeContentValid,
|
||||
resumeContentEnoughDetect
|
||||
} from '../../../../common/utils/resume'
|
||||
import {
|
||||
createReadNoReplyReminderLlmMockWindow,
|
||||
readNoReplyReminderLlmMockWindow
|
||||
} from '../../../window/readNoReplyReminderLlmMockWindow'
|
||||
import { RequestSceneEnum } from '../../../features/llm-request-log'
|
||||
|
||||
export default function initIpc() {
|
||||
ipcMain.on('open-external-link', (_, link) => {
|
||||
shell.openExternal(link, {
|
||||
activate: true
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('fetch-config-file-content', async () => {
|
||||
const configFileContentList = configFileNameList.map((fileName) => {
|
||||
return readConfigFile(fileName)
|
||||
@@ -69,8 +83,15 @@ export default function initIpc() {
|
||||
if (hasOwn(payload, 'anyCombineRecommendJobFilter')) {
|
||||
bossConfig.anyCombineRecommendJobFilter = payload.anyCombineRecommendJobFilter
|
||||
}
|
||||
if (hasOwn(payload, 'expectJobRegExpStr')) {
|
||||
bossConfig.expectJobRegExpStr = payload.expectJobRegExpStr
|
||||
delete bossConfig.expectJobRegExpStr
|
||||
if (hasOwn(payload, 'expectJobNameRegExpStr')) {
|
||||
bossConfig.expectJobNameRegExpStr = payload.expectJobNameRegExpStr
|
||||
}
|
||||
if (hasOwn(payload, 'expectJobTypeRegExpStr')) {
|
||||
bossConfig.expectJobTypeRegExpStr = payload.expectJobTypeRegExpStr
|
||||
}
|
||||
if (hasOwn(payload, 'expectJobDescRegExpStr')) {
|
||||
bossConfig.expectJobDescRegExpStr = payload.expectJobDescRegExpStr
|
||||
}
|
||||
if (hasOwn(payload, 'autoReminder')) {
|
||||
bossConfig.autoReminder = payload.autoReminder
|
||||
@@ -435,6 +456,130 @@ export default function initIpc() {
|
||||
return await getJobHistoryByEncryptId(encryptJobId)
|
||||
})
|
||||
|
||||
ipcMain.handle('llm-config', async () => {
|
||||
createLlmConfigWindow({
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
})
|
||||
const defer = Promise.withResolvers()
|
||||
async function saveLlmConfigHandler(_, configToSave) {
|
||||
await writeConfigFile('llm.json', configToSave)
|
||||
defer.resolve()
|
||||
ipcMain.removeHandler('save-llm-config')
|
||||
llmConfigWindow?.close()
|
||||
}
|
||||
ipcMain.handle('save-llm-config', saveLlmConfigHandler)
|
||||
llmConfigWindow?.once('closed', () => {
|
||||
ipcMain.removeHandler('save-llm-config')
|
||||
defer.reject(new Error('cancel'))
|
||||
})
|
||||
return defer.promise
|
||||
})
|
||||
ipcMain.on('close-llm-config', () => llmConfigWindow?.close())
|
||||
|
||||
ipcMain.handle('resume-edit', async () => {
|
||||
createResumeEditorWindow({
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
})
|
||||
const defer = Promise.withResolvers()
|
||||
async function saveResumeHandler(_, resumeContent) {
|
||||
await writeConfigFile('resumes.json', [
|
||||
{
|
||||
name: '默认简历',
|
||||
updateTime: Number(new Date()),
|
||||
content: resumeContent
|
||||
}
|
||||
])
|
||||
defer.resolve()
|
||||
resumeEditorWindow?.close()
|
||||
}
|
||||
ipcMain.handle('save-resume-content', saveResumeHandler)
|
||||
resumeEditorWindow?.once('closed', () => {
|
||||
ipcMain.removeHandler('save-resume-content')
|
||||
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', () => {
|
||||
app.exit(0)
|
||||
})
|
||||
|
||||
@@ -47,7 +47,10 @@ const payloadHandler = {
|
||||
const [data, totalItemCount] = await measureExecutionTime(
|
||||
userRepository.findAndCount({
|
||||
skip: (pageNo - 1) * pageSize,
|
||||
take: pageSize
|
||||
take: pageSize,
|
||||
order: {
|
||||
date: 'DESC'
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
@@ -69,7 +72,10 @@ const payloadHandler = {
|
||||
const [data, totalItemCount] = await measureExecutionTime(
|
||||
recordRepository.findAndCount({
|
||||
skip: (pageNo - 1) * pageSize,
|
||||
take: pageSize
|
||||
take: pageSize,
|
||||
order: {
|
||||
date: 'DESC'
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { sleepWithRandomDelay, sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { completes } from '@geekgeekrun/utils/gpt-request.mjs'
|
||||
import { recordGptCompletionRequest, RequestSceneEnum } from '../../features/llm-request-log'
|
||||
import {
|
||||
readConfigFile,
|
||||
readStorageFile,
|
||||
writeStorageFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { formatResumeJsonToMarkdown } from '../../../common/utils/resume'
|
||||
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../common/constant'
|
||||
import { LlmModelUsageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/LlmModelUsageRecord'
|
||||
import gtag from '../../utils/gtag'
|
||||
|
||||
export const sendLookForwardReplyEmotion = async (page: Page) => {
|
||||
const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion')
|
||||
@@ -15,3 +26,241 @@ export const sendLookForwardReplyEmotion = async (page: Page) => {
|
||||
)
|
||||
await lookForwardReplyEmojiProxy!.click()
|
||||
}
|
||||
|
||||
const pickLlmConfigFromList = (llmConfigList, blockModelSet) => {
|
||||
if (llmConfigList.length === 1) {
|
||||
llmConfigList[0].enabled = true
|
||||
llmConfigList[0].serveWeight = SINGLE_ITEM_DEFAULT_SERVE_WEIGHT
|
||||
}
|
||||
llmConfigList = llmConfigList.filter((it) => it.enabled && !blockModelSet.has(it.id))
|
||||
if (!llmConfigList.length) {
|
||||
return null
|
||||
}
|
||||
llmConfigList.forEach((conf) => {
|
||||
if (!Number(conf.serveWeight) || conf.serveWeight < 1) {
|
||||
conf.serveWeight = 1
|
||||
}
|
||||
if (conf.serveWeight > 100) {
|
||||
conf.serveWeight = 100
|
||||
}
|
||||
})
|
||||
const pool: number[] = []
|
||||
for (let i = 0; i < llmConfigList.length; i++) {
|
||||
for (let j = 0; j < Math.floor(llmConfigList[i].serveWeight); j++) {
|
||||
pool.push(llmConfigList[i].id)
|
||||
}
|
||||
}
|
||||
if (!pool.length) {
|
||||
return null
|
||||
}
|
||||
const index = Math.floor(pool.length * Math.random())
|
||||
return llmConfigList.find((it) => it.id === pool[index]) ?? null
|
||||
}
|
||||
|
||||
// let _index = 0
|
||||
|
||||
const RESUME_PLACEHOLDER = `__REPLACE_REAL_RESUME_HERE__`
|
||||
const defaultPrompt = `**核心指令:**
|
||||
你是一个智能求职助手,需要根据用户简历生成30字左右的提醒消息,满足以下要求:
|
||||
1. 每次生成需满足:
|
||||
- √ 包含1个核心技能 + 1个成果量化
|
||||
- √ 使用不同句式模板(至少准备5种)
|
||||
- √ 谦虚一些,头衔、工作年限等在历史记录信息中出现一次就好
|
||||
- ✗ 严禁与最近发送的几条相似或雷同
|
||||
- ✗ 严禁出现简历之外的词语
|
||||
- ✗ 严禁包含最近8条已经发过的内容(包括但不限于职位名称)
|
||||
|
||||
**简历分析层:**
|
||||
请从以下简历内容中提取关键要素:\n\`\`\`markdown\n${RESUME_PLACEHOLDER}\n\`\`\`\n
|
||||
|
||||
---
|
||||
要求提取:
|
||||
1. 硬技能:编程语言/技术栈/工具证书等(至少提取5项)
|
||||
2. 项目经历与成果:业绩、带量化数据的结果(至少3条)
|
||||
3. 软技能:沟通/管理等(至少2项)
|
||||
4. 特殊成就:奖项/专利等(可选)
|
||||
|
||||
**消息生成层:**
|
||||
根据上述要素随机组合生成消息
|
||||
|
||||
**质量控制层:**
|
||||
每次生成前执行:
|
||||
1. 检查历史记录
|
||||
2. 确保技能/成果组合未重复
|
||||
3. 确保所生成的新消息不包含最近8条已经发过的内容(包括但不限于职位名称)
|
||||
4. 字数严格控制在10-40字
|
||||
5. 避免感叹号等激进符号
|
||||
6. 减少头衔“资深”、“高级”出现的频率,严禁出现“专家”、“老兵”;减少工作年限“x年”出现的频率
|
||||
|
||||
**输出格式:**
|
||||
请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:\`{"response": "这里是将会发送给招聘者的内容"}\``
|
||||
|
||||
export const autoReminderPromptTemplateFileName = 'auto-reminder-resume-system-message-template.md'
|
||||
export const getValidTemplate = async () => {
|
||||
let template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false })
|
||||
if (!template) {
|
||||
await writeDefaultAutoRemindPrompt()
|
||||
template = defaultPrompt
|
||||
}
|
||||
if (!template.includes(RESUME_PLACEHOLDER)) {
|
||||
const e = new Error(`简历内容占位符字符串不存在。占位字符串是 ${RESUME_PLACEHOLDER}`)
|
||||
e.name = `RESUME_PLACEHOLDER_NOT_EXIST`
|
||||
throw e
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
export const writeDefaultAutoRemindPrompt = async () => {
|
||||
await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false })
|
||||
}
|
||||
|
||||
export const requestNewMessageContent = async (
|
||||
chatRecords,
|
||||
{
|
||||
requestScene,
|
||||
llmConfigIdForPick
|
||||
}: { requestScene?: RequestSceneEnum; llmConfigIdForPick?: string[] } = {}
|
||||
) => {
|
||||
const template = await getValidTemplate()
|
||||
const resumeObject = (await readConfigFile('resumes.json'))?.[0]
|
||||
const resumeContent = formatResumeJsonToMarkdown(resumeObject)
|
||||
const chatList = [
|
||||
{
|
||||
role: 'system',
|
||||
content: template.replace(RESUME_PLACEHOLDER, resumeContent)
|
||||
}
|
||||
]
|
||||
chatList.push({
|
||||
role: 'user',
|
||||
content:
|
||||
'请根据我的简历,帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容,但需要表达出应聘意向。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`'
|
||||
})
|
||||
// chatRecords = chatRecords.slice(chatRecords.length - _index)
|
||||
// debugger
|
||||
for (const record of chatRecords) {
|
||||
const assistantJsonContent = JSON.stringify({
|
||||
response: record.text
|
||||
})
|
||||
chatList.push({
|
||||
role: 'assistant',
|
||||
content: `\`\`\`json\n${assistantJsonContent}\n\`\`\``
|
||||
})
|
||||
chatList.push({
|
||||
role: 'user',
|
||||
content:
|
||||
'围绕我简历中关于自我介绍、技术栈、工作经历、项目描述、项目业绩等内容,写一句自我介绍。开头不必包含“您好”、结尾不必包含“期待回复”;务必确保本次所回复的内容不能与之前所回复的内容雷同或相似。请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`'
|
||||
})
|
||||
}
|
||||
console.log(chatList)
|
||||
let res, llmConfig
|
||||
const llmRequestRecord: Omit<LlmModelUsageRecord, 'id' | 'providerApiSecretMd5'> & {
|
||||
providerApiSecret: string
|
||||
} = {}
|
||||
const blockModelSet = new Set()
|
||||
while (!res) {
|
||||
let llmConfigList = await readConfigFile('llm.json')
|
||||
if (llmConfigIdForPick?.length) {
|
||||
llmConfigList = llmConfigList.filter((it) => {
|
||||
return llmConfigIdForPick.includes(it.id)
|
||||
})
|
||||
}
|
||||
llmConfig = pickLlmConfigFromList(llmConfigList, blockModelSet)
|
||||
if (!llmConfig) {
|
||||
throw new Error(`CANNOT_FIND_A_USABLE_MODEL`)
|
||||
}
|
||||
console.log(llmConfig.providerCompleteApiUrl)
|
||||
Object.assign(llmRequestRecord, {
|
||||
providerCompleteApiUrl: llmConfig.providerCompleteApiUrl,
|
||||
model: llmConfig.model,
|
||||
providerApiSecret: llmConfig.providerApiSecret,
|
||||
requestStartTime: new Date(),
|
||||
hasError: false,
|
||||
errorMessage: '',
|
||||
requestScene
|
||||
})
|
||||
try {
|
||||
const completion = await completes(
|
||||
{
|
||||
baseURL: llmConfig.providerCompleteApiUrl,
|
||||
apiKey: llmConfig.providerApiSecret,
|
||||
model: llmConfig.model
|
||||
},
|
||||
chatList
|
||||
)
|
||||
res = completion?.choices?.[0] ?? null
|
||||
Object.assign(llmRequestRecord, {
|
||||
completionTokens: completion.usage?.completion_tokens ?? null,
|
||||
promptCacheHitTokens: completion.usage?.prompt_cache_hit_tokens ?? null,
|
||||
promptCacheMissTokens: completion.usage?.prompt_cache_miss_tokens ?? null,
|
||||
promptTokens: completion.usage?.prompt_tokens ?? null,
|
||||
totalTokens: completion.usage?.total_tokens ?? null
|
||||
} as LlmModelUsageRecord)
|
||||
} catch (err) {
|
||||
console.log('request failed', err)
|
||||
blockModelSet.add(llmConfig.id)
|
||||
Object.assign(llmRequestRecord, {
|
||||
hasError: true,
|
||||
errorMessage: err?.message ?? ''
|
||||
})
|
||||
} finally {
|
||||
llmRequestRecord.requestEndTime = new Date()
|
||||
try {
|
||||
await recordGptCompletionRequest(llmRequestRecord)
|
||||
} catch (err) {
|
||||
console.log('CANNOT_SAVE_LLM_COMPLETION_LOG', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(res)
|
||||
// _index++
|
||||
let textToSend
|
||||
try {
|
||||
const rawMarkdownText = res?.message?.content
|
||||
try {
|
||||
textToSend = JSON.parse(
|
||||
rawMarkdownText.replace(/^```json/m, '').replace(/```$/m, '')
|
||||
)?.response
|
||||
} catch (err) {
|
||||
gtag('encounter_error_when_parse_llm_text', {
|
||||
err,
|
||||
model: llmConfig?.model,
|
||||
providerCompleteApiUrl: llmConfig?.providerCompleteApiUrl
|
||||
})
|
||||
throw err
|
||||
}
|
||||
textToSend = textToSend?.replace(/。$/, '')
|
||||
if (!textToSend) {
|
||||
gtag('llm_respond_text_is_empty', {
|
||||
model: llmConfig?.model,
|
||||
providerCompleteApiUrl: llmConfig?.providerCompleteApiUrl
|
||||
})
|
||||
throw new Error(`empty content. ${err?.message} ${res?.message?.content}`)
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`fail to parse response. ${err?.message} ${res?.message?.content}`)
|
||||
}
|
||||
return {
|
||||
responseText: textToSend,
|
||||
usedLlmConfig: llmConfig,
|
||||
recordInfo: llmRequestRecord
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendGptContent(page: Page, chatRecords) {
|
||||
const textToSend = (
|
||||
await requestNewMessageContent(chatRecords, {
|
||||
requestScene: RequestSceneEnum.readNoReplyAutoReminder
|
||||
})
|
||||
).responseText
|
||||
const chatInputSelector = `.chat-conversation .message-controls .chat-input`
|
||||
const chatInputHandle = await page.$(chatInputSelector)
|
||||
await chatInputHandle.click()
|
||||
await sleep(500)
|
||||
await chatInputHandle.click()
|
||||
await chatInputHandle.type(textToSend, {
|
||||
delay: 50
|
||||
})
|
||||
await sleep(1000)
|
||||
const sendButtonSelector = `.chat-conversation .message-controls .chat-op .btn-send:not(.disabled)`
|
||||
await page.click(sendButtonSelector)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { bootstrap, launchBoss } from './bootstrap'
|
||||
import { MsgStatus, type ChatListItem } from './types'
|
||||
import { Browser, Page } from 'puppeteer'
|
||||
import { sendLookForwardReplyEmotion } from './boss-operation'
|
||||
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
|
||||
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||
import { app } from 'electron'
|
||||
import { app, dialog } from 'electron'
|
||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||
import {
|
||||
getPublicDbFilePath,
|
||||
@@ -17,10 +17,21 @@ import * as fs from 'fs'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||
import { RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat'
|
||||
import gtag from '../../utils/gtag'
|
||||
|
||||
const throttleIntervalMinutes =
|
||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||
const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21
|
||||
const recentMessageQuantityForLlm =
|
||||
readConfigFile('boss.json').autoReminder?.recentMessageQuantityForLlm ?? 8
|
||||
const rechatContentSource =
|
||||
readConfigFile('boss.json').autoReminder?.rechatContentSource ??
|
||||
RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION
|
||||
const rechatLlmFallback =
|
||||
readConfigFile('boss.json').autoReminder?.rechatLlmFallback ??
|
||||
RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
|
||||
|
||||
const dbInitPromise = initDb(getPublicDbFilePath())
|
||||
|
||||
export const pageMapByName: {
|
||||
@@ -172,6 +183,9 @@ const mainLoop = async () => {
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await pageMapByName.boss?.waitForFunction(() => {
|
||||
return Array.isArray(document.querySelector('.main-wrap .chat-user')?.__vue__?.list)
|
||||
})
|
||||
// find target boss - with unread icon, or recommend system message
|
||||
const friendListData = (await pageMapByName.boss!.evaluate(
|
||||
`
|
||||
@@ -277,7 +291,31 @@ const mainLoop = async () => {
|
||||
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
|
||||
) {
|
||||
await sleepWithRandomDelay(3250)
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
|
||||
try {
|
||||
const messageListForGpt = historyMessageList
|
||||
.filter((it) => it.bizType !== 101 && it.isSelf)
|
||||
.slice(-recentMessageQuantityForLlm)
|
||||
await sendGptContent(pageMapByName.boss!, messageListForGpt)
|
||||
gtag('rnrr_llm_content_sent')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) {
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
gtag('rnrr_look_forward_reply_emotion_sent', {
|
||||
fallback: true
|
||||
})
|
||||
} else {
|
||||
gtag('rnrr_encounter_error', {
|
||||
error: err
|
||||
})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await sendLookForwardReplyEmotion(pageMapByName.boss!)
|
||||
gtag('rnrr_look_forward_reply_emotion_sent')
|
||||
}
|
||||
} else {
|
||||
cursorToContinueFind += 1
|
||||
}
|
||||
@@ -337,6 +375,17 @@ export async function runEntry() {
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
if (err instanceof Error && err.message === 'CANNOT_FIND_A_USABLE_MODEL') {
|
||||
gtag('cannot_find_a_usable_model')
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
message:
|
||||
'未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题',
|
||||
buttons: ['退出']
|
||||
})
|
||||
process.exit(0)
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
pageMapByName['boss'] = null
|
||||
await sleep(rerunInterval)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import buildInfo from '../../../common/build-info.json'
|
||||
import os from 'node:os'
|
||||
|
||||
type LowercaseLetter =
|
||||
| 'a'
|
||||
@@ -55,6 +56,7 @@ function getCommonParams() {
|
||||
return {
|
||||
app_version: buildInfo.version,
|
||||
app_build_hash: buildInfo.buildHash,
|
||||
os_info: `${os.type()} | ${os.release()} | ${os.arch()}`,
|
||||
t: Number(new Date())
|
||||
}
|
||||
}
|
||||
@@ -68,9 +70,14 @@ export default async function gtag<T extends string>(
|
||||
...getCommonParams(),
|
||||
...params
|
||||
}
|
||||
Object.keys(params).forEach((k) => {
|
||||
if ([null, undefined].includes(params[k])) {
|
||||
delete params[k]
|
||||
}
|
||||
})
|
||||
// ServiceWorker环境下直接调用上报函数
|
||||
const reporter = (await import('./Analytics')).default
|
||||
return reporter.fireEvent(name, params)
|
||||
return reporter.fireEvent(name.replace(/-/g, '_'), params)
|
||||
}
|
||||
|
||||
// Fire a page view event.
|
||||
|
||||
58
packages/ui/src/main/utils/initPublicIpc.ts
Normal file
58
packages/ui/src/main/utils/initPublicIpc.ts
Normal file
@@ -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()
|
||||
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()
|
||||
|
||||
45
packages/ui/src/main/window/llmConfigWindow.ts
Normal file
45
packages/ui/src/main/window/llmConfigWindow.ts
Normal file
@@ -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.show()
|
||||
})
|
||||
@@ -36,6 +34,12 @@ export function createMainWindow(): BrowserWindow {
|
||||
show: true
|
||||
})
|
||||
})
|
||||
mainWindow.on('ready-to-show', async () => {
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
setTimeout(() => {
|
||||
mainWindow && openDevTools(mainWindow)
|
||||
}, 500)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
|
||||
@@ -0,0 +1,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!
|
||||
}
|
||||
45
packages/ui/src/main/window/resumeEditorWindow.ts
Normal file
45
packages/ui/src/main/window/resumeEditorWindow.ts
Normal file
@@ -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">
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import { onMounted } from 'vue'
|
||||
import { gtagRenderer } from './utils/gtag'
|
||||
|
||||
gtagRenderer('app_component_before_create')
|
||||
onMounted(() => {
|
||||
gtagRenderer('app_component_mounted')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
<template #header>
|
||||
<div class="diff-table-header">
|
||||
{{ transformUtcDateToLocalDate(item.value).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
<el-tooltip content="待对比条目少于2个" :disabled="tableProps.length > 1">
|
||||
<el-tooltip
|
||||
content="待对比条目少于2个"
|
||||
:disabled="tableProps.length > 1"
|
||||
@show="gtagRenderer('tooltip_show_about_lake_of_compare_item')"
|
||||
>
|
||||
<el-radio v-model="diffPivot" :label="item.value" :disabled="tableProps.length <= 1">作为diff基准</el-radio>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -44,6 +48,7 @@ import { JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/src/entity/JobInfoC
|
||||
import { ElTable, ElTableColumn, ElForm, ElFormItem, ElRow, ElCol, ElDivider } from 'element-plus'
|
||||
import TextDiff from '../../components/TextDiff.vue'
|
||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const props = defineProps({
|
||||
jobInfo: {
|
||||
|
||||
@@ -3,9 +3,20 @@
|
||||
<el-form-item label="公司">{{ jobInfo.companyName }}</el-form-item>
|
||||
<el-form-item label="职位名称">{{ jobInfo.jobName }}</el-form-item>
|
||||
<el-form-item label="职位分类">{{ jobInfo.positionName }}</el-form-item>
|
||||
<el-form-item label="开聊时间">{{
|
||||
transformUtcDateToLocalDate(jobInfo.date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}}</el-form-item>
|
||||
<el-form-item v-if="scene === 'startChatRecord'" label="开聊时间">
|
||||
{{
|
||||
jobInfo.date
|
||||
? transformUtcDateToLocalDate(jobInfo.date).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '无记录'
|
||||
}}
|
||||
</el-form-item>
|
||||
<el-form-item v-if="scene === 'markAsNotSuitRecord'" label="标记时间">
|
||||
{{
|
||||
jobInfo.date
|
||||
? transformUtcDateToLocalDate(jobInfo.date).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '无记录'
|
||||
}}
|
||||
</el-form-item>
|
||||
<el-form-item label="工作经验">{{ jobInfo.experienceName }}</el-form-item>
|
||||
<el-form-item label="薪资">{{
|
||||
`${jobInfo.salaryLow}-${jobInfo.salaryHigh}k` +
|
||||
@@ -14,19 +25,26 @@
|
||||
<el-form-item label="职位描述">
|
||||
<pre class="of-auto">{{ jobInfo.description }}</pre>
|
||||
</el-form-item>
|
||||
<el-form-item label="BOSS">{{ jobInfo.bossName }} - {{ jobInfo.bossTitle }}</el-form-item>
|
||||
<el-form-item label="BOSS"
|
||||
>{{ jobInfo.bossName
|
||||
}}<template v-if="jobInfo.bossTitle"> - {{ jobInfo.bossTitle }}</template></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
||||
import { type VMarkAsNotSuitLog } from '@geekgeekrun/sqlite-plugin/src/entity/VMarkAsNotSuitLog'
|
||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
jobInfo: {
|
||||
type: Object as PropType<VChatStartupLog>,
|
||||
type: Object as PropType<VChatStartupLog | VMarkAsNotSuitLog>,
|
||||
required: true
|
||||
},
|
||||
scene: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -33,10 +34,12 @@ const checkDependenciesResult = ref({})
|
||||
const downloadProcessWaitee = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
gtagRenderer('bootstrap_mounted')
|
||||
checkDependenciesResult.value = await electron.ipcRenderer.invoke('check-dependencies')
|
||||
downloadProcessWaitee.value = Promise.withResolvers()
|
||||
|
||||
if (Object.values(checkDependenciesResult.value).includes(false)) {
|
||||
gtagRenderer('dependencies_need_download')
|
||||
router.replace('/downloadingDependencies')
|
||||
} else {
|
||||
downloadProcessWaitee.value!.resolve()
|
||||
@@ -45,10 +48,11 @@ onMounted(async () => {
|
||||
downloadProcessWaitee.value!.promise.then(async () => {
|
||||
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
|
||||
if (!isCookieFileValid) {
|
||||
gtagRenderer('found_cookie_invalid_when_bootstrap')
|
||||
router.replace('/cookieAssistant')
|
||||
} else {
|
||||
await sleep(1000)
|
||||
router.replace('/configuration')
|
||||
router.replace('/main-layout')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onUnmounted, PropType } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const props = defineProps({
|
||||
dependenciesStatus: {
|
||||
@@ -47,9 +48,11 @@ const processDownloadBrowser = async () => {
|
||||
const promiseList: Array<Promise<void>> = []
|
||||
const processTasks = async () => {
|
||||
if (!props.dependenciesStatus.puppeteerExecutableAvailable) {
|
||||
gtagRenderer('start_download_puppeteer')
|
||||
const p = processDownloadBrowser()
|
||||
promiseList.push(p)
|
||||
p.then(() => {
|
||||
gtagRenderer('puppeteer_download_success')
|
||||
props.dependenciesStatus.puppeteerExecutableAvailable = true
|
||||
})
|
||||
}
|
||||
@@ -64,6 +67,7 @@ const processTasks = async () => {
|
||||
})
|
||||
await p
|
||||
} catch {
|
||||
gtagRenderer('encounter_error_when_download_deps')
|
||||
await ElMessageBox.confirm('需要重试吗?', '核心组件下载失败', {
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
@@ -72,9 +76,11 @@ const processTasks = async () => {
|
||||
cancelButtonText: '退出程序'
|
||||
})
|
||||
.then(() => {
|
||||
gtagRenderer('start_retry_download_deps')
|
||||
processTasks()
|
||||
})
|
||||
.catch(() => {
|
||||
gtagRenderer('cancel_download_deps_and_exit')
|
||||
promiseList.length = 0
|
||||
electron.ipcRenderer.invoke('exit-app-immediately')
|
||||
})
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<div class="form-wrap">
|
||||
<el-form ref="formRef" :model="formContent" label-position="top" :rules="formRules">
|
||||
<el-form-item label="BOSS直聘 Cookie">
|
||||
<el-button size="small" type="primary" font-size-inherit @click="handleClickLaunchLogin"
|
||||
>编辑Cookie</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="钉钉机器人 AccessToken(用于记录开聊,请勿使用公司内部群)"
|
||||
prop="dingtalkRobotAccessToken"
|
||||
>
|
||||
<el-input v-model="formContent.dingtalkRobotAccessToken" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="期望职位白名单正则(按照职位名称+职位描述筛选职位,为空时将不按此条件筛选)"
|
||||
prop="expectJobRegExpStr"
|
||||
>
|
||||
<el-input v-model="formContent.expectJobRegExpStr" />
|
||||
</el-form-item>
|
||||
<el-form-item label="期望公司(以逗号分隔,为空时将不按此条件筛选)" prop="expectCompanies">
|
||||
<el-input
|
||||
v-model="formContent.expectCompanies"
|
||||
:autosize="{ minRows: 4 }"
|
||||
type="textarea"
|
||||
@blur="handleExpectCompaniesInputBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="推荐职位筛选器(当前求职期望找不到合适职位时,将尝试所有可能的筛选组合,查找新工作)"
|
||||
prop="filter"
|
||||
>
|
||||
<AnyCombineBossRecommendFilter v-model="formContent.anyCombineRecommendJobFilter" />
|
||||
<div>
|
||||
当前组合条件数:{{ currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString() }}
|
||||
<span
|
||||
v-if="
|
||||
currentAnyCombineRecommendJobFilterCombinationCount >= 10 &&
|
||||
currentAnyCombineRecommendJobFilterCombinationCount < 100
|
||||
"
|
||||
class="color-orange"
|
||||
>组合条件太多,建议少选择一些</span
|
||||
>
|
||||
<span
|
||||
v-if="currentAnyCombineRecommendJobFilterCombinationCount >= 100"
|
||||
class="color-orange"
|
||||
>你开心就好</span
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="标记不合适机制" class="color-orange">
|
||||
1. 如果查找到的职位活跃时间为“本月活跃”或更往前的时间,则这个职位将被标记为不合适<br />
|
||||
2. 如果查找到的职位,职位名称、职位类型、职位描述与期望职位白名单正则不匹配,则这个职位将被标记为不合适
|
||||
</el-form-item>
|
||||
<el-form-item class="last-form-item">
|
||||
<el-button @click="handleSave">仅保存配置</el-button>
|
||||
<el-button type="primary" @click="handleSubmit"> 保存配置,并开始求职! </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElForm, ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AnyCombineBossRecommendFilter from '@renderer/features/AnyCombineBossRecommendFilter/index.vue'
|
||||
import { calculateTotalCombinations } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
|
||||
const router = useRouter()
|
||||
|
||||
const formContent = ref({
|
||||
dingtalkRobotAccessToken: '',
|
||||
expectCompanies: '',
|
||||
anyCombineRecommendJobFilter: {},
|
||||
expectJobRegExpStr: ''
|
||||
})
|
||||
|
||||
const currentAnyCombineRecommendJobFilterCombinationCount = computed(() => {
|
||||
return calculateTotalCombinations(formContent.value.anyCombineRecommendJobFilter)
|
||||
})
|
||||
|
||||
electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
|
||||
console.log(res)
|
||||
formContent.value.dingtalkRobotAccessToken = res.config['dingtalk.json']['groupRobotAccessToken']
|
||||
formContent.value.expectCompanies = res.config['target-company-list.json'].join(',')
|
||||
formContent.value.anyCombineRecommendJobFilter = res.config['boss.json']
|
||||
?.anyCombineRecommendJobFilter ?? {
|
||||
salaryList: [],
|
||||
experienceList: [],
|
||||
degreeList: [],
|
||||
scaleList: [],
|
||||
industryList: []
|
||||
}
|
||||
formContent.value.expectJobRegExpStr = res.config['boss.json']?.expectJobRegExpStr ?? ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
expectJobRegExpStr: {
|
||||
validator(_, value, cb) {
|
||||
if (!value) {
|
||||
cb()
|
||||
return
|
||||
}
|
||||
try {
|
||||
new RegExp(value, 'ig')
|
||||
cb()
|
||||
} catch (err) {
|
||||
cb(new Error(`正则无效:${err.message}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
const handleSubmit = async () => {
|
||||
formContent.value.expectJobRegExpStr = (formContent.value.expectJobRegExpStr || '').trim()
|
||||
await formRef.value!.validate()
|
||||
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
|
||||
|
||||
router.replace({
|
||||
path: '/geekAutoStartChatWithBoss/prepareRun',
|
||||
query: { flow: 'geek-auto-start-chat-with-boss' }
|
||||
})
|
||||
}
|
||||
const handleSave = async () => {
|
||||
await formRef.value!.validate()
|
||||
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
|
||||
ElMessage.success('Configuration saved.')
|
||||
}
|
||||
|
||||
const handleExpectCompaniesInputBlur = (event) => {
|
||||
event.target.value = (event.target?.value ?? '')
|
||||
.split(/,|,/)
|
||||
.map((it) => it.trim())
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
const handleClickLaunchLogin = () => {
|
||||
router.replace('/cookieAssistant')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-wrap {
|
||||
margin: 0 auto;
|
||||
max-width: 1000px;
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
:deep(.el-form) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
.last-form-item {
|
||||
:deep(.el-form-item__content) {
|
||||
margin-top: 0px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,176 +0,0 @@
|
||||
<template>
|
||||
<div class="form-wrap">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:rules="formRules"
|
||||
:model="formContent.autoReminder"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item label="BOSS直聘 Cookie">
|
||||
<el-button size="small" type="primary" font-size-inherit @click="handleClickLaunchLogin"
|
||||
>编辑Cookie</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进话术" class="color-orange">
|
||||
当发现已读不回的Boss时,将向Boss发出“[盼回复]”表情
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
|
||||
<el-input-number
|
||||
v-model="formContent.autoReminder.throttleIntervalMinutes"
|
||||
class="w-150px"
|
||||
:min="3"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
@blur="handleThrottleIntervalMinutesBlur"
|
||||
/> 分钟内不多次跟进同一Boss
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进时限(天)" prop="rechatLimitDay">
|
||||
<div>
|
||||
<div><el-checkbox v-model="enableRechatLimit" /> 启用</div>
|
||||
<el-input-number
|
||||
v-model="formContent.autoReminder.rechatLimitDay"
|
||||
class="w-150px"
|
||||
:min="0"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
:disabled="!enableRechatLimit"
|
||||
/> 天<br />
|
||||
<div v-if="enableRechatLimit">
|
||||
不再跟进 (<span class="text-orange">{{ rechatLimitDateString }}</span>)之前列表中没有进展的聊天
|
||||
</div>
|
||||
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span>)</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item class="last-form-item">
|
||||
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { dayjs, ElForm } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
|
||||
const formContent = ref({
|
||||
autoReminder: {
|
||||
throttleIntervalMinutes: 10,
|
||||
rechatLimitDay: 21
|
||||
}
|
||||
})
|
||||
|
||||
const enableRechatLimit = computed({
|
||||
get() {
|
||||
return Boolean(formContent.value.autoReminder?.rechatLimitDay)
|
||||
},
|
||||
set(val) {
|
||||
if (!val) {
|
||||
formContent.value.autoReminder.rechatLimitDay = 0
|
||||
} else {
|
||||
formContent.value.autoReminder.rechatLimitDay = 21
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
|
||||
const conf = res.config['boss.json']?.autoReminder || {}
|
||||
conf.throttleIntervalMinutes = conf.throttleIntervalMinutes ?? 10
|
||||
conf.rechatLimitDay = conf.rechatLimitDay ?? 21
|
||||
|
||||
formContent.value.autoReminder = conf
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
throttleIntervalMinutes: {
|
||||
validator(_, value, cb) {
|
||||
if (/[^0-9.]/.test(String(value)) || isNaN(parseFloat(value)) || isNaN(Number(value))) {
|
||||
cb(new Error(`请输入数字!`))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
},
|
||||
rechatLimitDay: {
|
||||
validator(_, value, cb) {
|
||||
if (/[^0-9.]/.test(String(value)) || isNaN(parseFloat(value)) || isNaN(Number(value))) {
|
||||
cb(new Error(`请输入数字!`))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
watch(
|
||||
() => formContent.value.autoReminder,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
formRef.value?.validate?.()
|
||||
})
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value!.validate()
|
||||
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
|
||||
router.replace({
|
||||
path: '/geekAutoStartChatWithBoss/prepareRun',
|
||||
query: { flow: 'read-no-reply-reminder' }
|
||||
})
|
||||
}
|
||||
function handleThrottleIntervalMinutesBlur() {
|
||||
if (formContent.value.autoReminder.throttleIntervalMinutes < 3) {
|
||||
formContent.value.autoReminder.throttleIntervalMinutes = 3
|
||||
}
|
||||
formContent.value.autoReminder.throttleIntervalMinutes = Number(
|
||||
formContent.value.autoReminder.throttleIntervalMinutes
|
||||
)
|
||||
}
|
||||
|
||||
const handleClickLaunchLogin = () => {
|
||||
router.replace('/cookieAssistant')
|
||||
}
|
||||
|
||||
const currentStamp = ref(new Date())
|
||||
let timer = 0
|
||||
function updateCurrentStamp() {
|
||||
currentStamp.value = new Date()
|
||||
timer = window.setTimeout(updateCurrentStamp, 1000)
|
||||
}
|
||||
updateCurrentStamp()
|
||||
onUnmounted(() => {
|
||||
window.clearTimeout(timer)
|
||||
})
|
||||
|
||||
const rechatLimitDateString = computed(() => {
|
||||
return dayjs(
|
||||
+currentStamp.value - formContent.value.autoReminder.rechatLimitDay * 24 * 60 * 60 * 1000
|
||||
).format('YYYY-MM-DD HH:mm:ss')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-wrap {
|
||||
margin: 0 auto;
|
||||
max-width: 1000px;
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
:deep(.el-form) {
|
||||
padding-top: 40px;
|
||||
}
|
||||
.last-form-item {
|
||||
:deep(.el-form-item__content) {
|
||||
margin-top: 40px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h100vh">
|
||||
<div class="flex flex-col w160px pt30px pl30px aside-nav of-hidden">
|
||||
<div class="nav-list flex-1 of-auto">
|
||||
<RouterLink to="./GeekAutoStartChatWithBoss">Boss炸弹</RouterLink>
|
||||
<RouterLink to="./ReadNoReplyReminder">已读不回提醒器</RouterLink>
|
||||
<hr />
|
||||
<a href="javascript:void(0)" @click="handleLaunchBossSite">
|
||||
手动逛Boss<el-icon><TopRight /></el-icon>
|
||||
</a>
|
||||
<hr />
|
||||
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
|
||||
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
|
||||
<RouterLink to="./JobLibrary">职位库</RouterLink>
|
||||
<RouterLink to="./BossLibrary">Boss库</RouterLink>
|
||||
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
|
||||
</div>
|
||||
<div class="pt-16px pb-16px flex-0 font-size-12px">
|
||||
<div>当前版本: {{ buildInfo.version }}({{ buildInfo.buildVersion }})</div>
|
||||
<div class="feedback-area flex flex-items-center mt-8px">
|
||||
<el-button type="text" size="small" @click="handleGotoProjectPageClick"
|
||||
>项目首页</el-button
|
||||
>
|
||||
|
|
||||
<el-button type="text" size="small" @click="handleFeedbackClick">反馈问题</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RouterView v-slot="{ Component }" class="flex-1">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { TopRight } from '@element-plus/icons-vue'
|
||||
import useBuildInfo from '@renderer/hooks/useBuildInfo'
|
||||
import { debounce } from 'lodash-es'
|
||||
const router = useRouter()
|
||||
const unmountedCbs: Array<InstanceType<typeof Function>> = []
|
||||
onUnmounted(() => {
|
||||
while (unmountedCbs.length) {
|
||||
const fn = unmountedCbs.shift()!
|
||||
try {
|
||||
fn()
|
||||
} catch {}
|
||||
}
|
||||
})
|
||||
const goToCheckBossZhipinCookieFile = () => router.replace('/cookieAssistant')
|
||||
onMounted(() => {
|
||||
electron.ipcRenderer.on('check-boss-zhipin-cookie-file', goToCheckBossZhipinCookieFile)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
electron.ipcRenderer.removeListener(
|
||||
'check-boss-zhipin-cookie-file',
|
||||
goToCheckBossZhipinCookieFile
|
||||
)
|
||||
})
|
||||
;(async () => {
|
||||
const checkDependenciesResult = await electron.ipcRenderer.invoke('check-dependencies')
|
||||
if (Object.values(checkDependenciesResult).includes(false)) {
|
||||
router.replace('/')
|
||||
return
|
||||
}
|
||||
|
||||
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
|
||||
if (!isCookieFileValid) {
|
||||
router.replace('/cookieAssistant')
|
||||
return
|
||||
}
|
||||
})()
|
||||
|
||||
const { buildInfo } = useBuildInfo()
|
||||
const getIssueUrlWithBody = (issueBody: string = '') => {
|
||||
const baseUrl = `https://github.com/geekgeekrun/geekgeekrun/issues/new`
|
||||
issueBody = issueBody || ''
|
||||
if (!issueBody || !issueBody.trim()) {
|
||||
return baseUrl
|
||||
}
|
||||
const urlObj = new URL(baseUrl)
|
||||
urlObj.searchParams.append('body', issueBody)
|
||||
|
||||
return urlObj.toString()
|
||||
}
|
||||
const handleFeedbackClick = () => {
|
||||
electron.ipcRenderer.send(
|
||||
'open-external-link',
|
||||
getIssueUrlWithBody(`\n\n\n-----
|
||||
版本号:${buildInfo.value.version}(${buildInfo.value.buildVersion})
|
||||
提交:${buildInfo.value.buildHash.substring(0, 6)}`)
|
||||
)
|
||||
}
|
||||
const handleGotoProjectPageClick = () => {
|
||||
electron.ipcRenderer.send('open-external-link', 'https://github.com/geekgeekrun/geekgeekrun')
|
||||
}
|
||||
|
||||
const handleLaunchBossSite = debounce(
|
||||
async () => {
|
||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||
url: `https://www.zhipin.com/`
|
||||
})
|
||||
},
|
||||
1000,
|
||||
{ leading: true, trailing: false }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.aside-nav {
|
||||
background-image: linear-gradient(45deg, #eaf4f1, #dcf6f2);
|
||||
.nav-list {
|
||||
> a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.5em;
|
||||
box-sizing: border-box;
|
||||
padding-left: 2em;
|
||||
&.router-link-active {
|
||||
background-color: #fff;
|
||||
font-weight: 700;
|
||||
color: #2faa9e;
|
||||
border-radius: 9999px 0 0 9999px;
|
||||
}
|
||||
}
|
||||
> hr {
|
||||
border: 0 solid;
|
||||
height: 1px;
|
||||
background-color: #b3c8c3;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
width: 140px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.feedback-area {
|
||||
:deep(.el-button) {
|
||||
height: fit-content;
|
||||
padding: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="cookie-assistant-page">
|
||||
<div ml1em mt1em mb1em >Cookie 助手</div>
|
||||
<div ml1em mt1em mb1em font-size-16px>Cookie 助手</div>
|
||||
<el-alert
|
||||
v-if="cookieInvalid"
|
||||
type="warning"
|
||||
@@ -11,11 +11,11 @@
|
||||
</el-alert>
|
||||
<div ml1em mt1em line-height-normal>
|
||||
如果您了解如何获取Cookie、了解有效的Cookie格式,可以直接在下方输入框中进行编辑。由于手动编辑较为麻烦,建议您打开已登录过Boss直聘的浏览器,使用<a
|
||||
color-blue
|
||||
decoration-none
|
||||
class="color-blue! decoration-none"
|
||||
href="javascript:void(0)"
|
||||
@click.prevent="handleEditThisCookieExtensionStoreLinkClick"
|
||||
>EditThisCookie 扩展程序</a
|
||||
>
|
||||
EditThisCookie 扩展程序 </a
|
||||
>复制Cookie,然后粘贴在下方输入框中。文本格式为被序列化为JSON的数组,不含两侧引号。
|
||||
</div>
|
||||
<br />
|
||||
@@ -85,7 +85,12 @@
|
||||
size="small"
|
||||
type="primary"
|
||||
font-size-inherit
|
||||
@click="fillCollectedCookie"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('replace_inputted_cookie_by_collected')
|
||||
fillCollectedCookie()
|
||||
}
|
||||
"
|
||||
>使用获取到的Cookie</el-button
|
||||
></template
|
||||
></el-alert
|
||||
@@ -103,7 +108,8 @@
|
||||
import { ElForm, ElMessage } from 'element-plus'
|
||||
import { ref, onUnmounted, onMounted } from 'vue'
|
||||
import { checkCookieListFormat } from '../../../../common/utils/cookie'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter } from 'vue-router'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const router = useRouter()
|
||||
const cookieInvalid = ref(false)
|
||||
@@ -138,11 +144,13 @@ const formRules = {
|
||||
`JSON格式无效 - 存在语法错误: ${err.message};建议使用EditThisCookie扩展程序进行复制。`
|
||||
)
|
||||
)
|
||||
gtagRenderer('wrong_cookie_format_json_syntax_error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!checkCookieListFormat(JSON.parse(formContent.value.collectedCookies))) {
|
||||
cb(new Error(`Cookie格式无效 - 部分字段缺失;建议使用EditThisCookie扩展程序进行复制。`))
|
||||
gtagRenderer('wrong_cookie_format_field_loss')
|
||||
return
|
||||
}
|
||||
cb()
|
||||
@@ -158,6 +166,9 @@ const handleCookieCollected = (_, payload) => {
|
||||
collectedCookie.value = payload.cookies
|
||||
if (!hasUserMutateInput.value) {
|
||||
fillCollectedCookie()
|
||||
gtagRenderer('cookie_collected_and_auto_filled')
|
||||
} else {
|
||||
gtagRenderer('cookie_collected_after_changed_input')
|
||||
}
|
||||
}
|
||||
const fillCollectedCookie = () => {
|
||||
@@ -169,28 +180,33 @@ const fillCollectedCookie = () => {
|
||||
}
|
||||
|
||||
const handleClickLaunchLogin = () => {
|
||||
gtagRenderer('launch_login_button_clicked')
|
||||
electron.ipcRenderer.send('launch-bosszhipin-login-page-with-preload-extension')
|
||||
loginCookieWaitingStatus.value = LOGIN_COOKIE_WAITING_STATUS.WAITING_FOR_LOGIN
|
||||
}
|
||||
|
||||
const handleEditThisCookieExtensionStoreLinkClick = () => {
|
||||
gtagRenderer('etc_extension_link_clicked')
|
||||
electron.ipcRenderer.send(
|
||||
'open-external-link',
|
||||
'https://chromewebstore.google.com/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg'
|
||||
'https://chromewebstore.google.com/detail/editthiscookie-v3/ojfebgpkimhlhcblbalbfjblapadhbol'
|
||||
)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
router.replace('/configuration')
|
||||
gtagRenderer('cancel_clicked')
|
||||
router.replace('/main-layout')
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
gtagRenderer('save_clicked')
|
||||
await formRef.value!.validate()
|
||||
await electron.ipcRenderer.invoke('write-storage-file', {
|
||||
fileName: 'boss-cookies.json',
|
||||
data: formContent.value.collectedCookies
|
||||
})
|
||||
ElMessage.success('Boss直聘 Cookie 保存成功')
|
||||
router.replace('/configuration')
|
||||
gtagRenderer('save_cookie_done')
|
||||
router.replace('/main-layout')
|
||||
}
|
||||
|
||||
const handleBossZhipinLoginPageClosed = () => {
|
||||
@@ -199,6 +215,9 @@ const handleBossZhipinLoginPageClosed = () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gtagRenderer('cookie_assistant_mounted')
|
||||
})
|
||||
onMounted(async () => {
|
||||
electron.ipcRenderer.once('BOSS_ZHIPIN_COOKIE_COLLECTED', handleCookieCollected)
|
||||
electron.ipcRenderer.on('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED', handleBossZhipinLoginPageClosed)
|
||||
@@ -226,6 +245,7 @@ onUnmounted(() => {
|
||||
.cookie-assistant-page {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus'
|
||||
import { ref, onMounted, onBeforeMount } from 'vue';
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const electron = window.electron
|
||||
|
||||
const readmeItemCheckStatusList = ref<number[]>([])
|
||||
@@ -97,8 +99,10 @@ const handleCancel = () => {
|
||||
|
||||
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
|
||||
const handleSubmit = () => {
|
||||
gtagRenderer('submit_clicked')
|
||||
const COUNT = 9
|
||||
if (readmeItemCheckStatusList.value.length !== COUNT) {
|
||||
gtagRenderer('agreement_not_finish_read_tip_displayed')
|
||||
ElMessage.warning(
|
||||
`您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`
|
||||
)
|
||||
@@ -111,6 +115,7 @@ const handleSubmit = () => {
|
||||
return
|
||||
}
|
||||
electron.ipcRenderer.invoke('first-launch-notice-approve')
|
||||
gtagRenderer('submit_done')
|
||||
}
|
||||
const handleReadmeItemCheckStatusListChange = (value: number[]) => {
|
||||
value.forEach((it) => {
|
||||
|
||||
@@ -18,22 +18,26 @@ import { ref, onUnmounted, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FlyingCompanyLogoList from '../../features/FlyingCompanyLogoList/index.vue'
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const { ipcRenderer } = electron
|
||||
const router = useRouter()
|
||||
|
||||
const handleStopButtonClick = async () => {
|
||||
gtagRenderer('gascwb_stop_button_clicked')
|
||||
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
|
||||
}
|
||||
|
||||
const isStopping = ref(false)
|
||||
const handleStopping = () => {
|
||||
gtagRenderer('gascwb_become_stopping')
|
||||
isStopping.value = true
|
||||
}
|
||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
||||
|
||||
const handleStopped = () => {
|
||||
router.replace('/configuration/GeekAutoStartChatWithBoss')
|
||||
gtagRenderer('gascwb_become_stopped')
|
||||
router.replace('/main-layout/GeekAutoStartChatWithBoss')
|
||||
}
|
||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopped', handleStopped)
|
||||
|
||||
@@ -47,12 +51,14 @@ onMounted(async () => {
|
||||
await electron.ipcRenderer.invoke('run-geek-auto-start-chat-with-boss')
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
||||
gtagRenderer('gascwb_cannot_run_for_corrupt')
|
||||
ElMessage.error({
|
||||
message: `核心组件损坏,正在尝试修复`
|
||||
})
|
||||
router.replace('/')
|
||||
}
|
||||
console.error(err)
|
||||
gtagRenderer('gascwb_cannot_run_for_unknown_error', { err })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -16,22 +16,26 @@ import { ref, onUnmounted, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FlyingCompanyLogoList from '../../features/FlyingCompanyLogoList/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const { ipcRenderer } = electron
|
||||
const router = useRouter()
|
||||
|
||||
const handleStopButtonClick = async () => {
|
||||
gtagRenderer('rnrr_stop_button_clicked')
|
||||
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
|
||||
}
|
||||
|
||||
const isStopping = ref(false)
|
||||
const handleStopping = () => {
|
||||
gtagRenderer('rnrr_become_stopping')
|
||||
isStopping.value = true
|
||||
}
|
||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
|
||||
|
||||
const handleStopped = () => {
|
||||
router.replace('/configuration/ReadNoReplyReminder')
|
||||
gtagRenderer('rnrr_become_stopped')
|
||||
router.replace('/main-layout/ReadNoReplyReminder')
|
||||
}
|
||||
ipcRenderer.once('geek-auto-start-chat-with-boss-stopped', handleStopped)
|
||||
|
||||
@@ -45,12 +49,14 @@ onMounted(async () => {
|
||||
await electron.ipcRenderer.invoke('run-read-no-reply-auto-reminder')
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
|
||||
gtagRenderer('rnrr_cannot_run_for_corrupt')
|
||||
ElMessage.error({
|
||||
message: `核心组件损坏,正在尝试修复`
|
||||
})
|
||||
router.replace('/')
|
||||
}
|
||||
console.error(err)
|
||||
gtagRenderer('rnrr_cannot_run_for_unknown_error', { err })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
563
packages/ui/src/renderer/src/page/LlmConfig/index.vue
Normal file
563
packages/ui/src/renderer/src/page/LlmConfig/index.vue
Normal file
@@ -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>
|
||||
@@ -19,7 +19,17 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getBossLibrary">刷新</el-button>
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('boss_library_refresh_clicked')
|
||||
getBossLibrary()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.pageNo"
|
||||
@@ -42,6 +52,7 @@ import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ElTable, ElTableColumn, ElButton, ElPagination } from 'element-plus'
|
||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VChatStartupLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -54,6 +65,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getBossLibrary() {
|
||||
try {
|
||||
gtagRenderer('boss_library_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-boss-library', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -65,7 +80,16 @@ async function getBossLibrary() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('boss_library_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('boss_library_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
@@ -23,7 +23,15 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getCompanyLibrary"
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('company_library_refresh_clicked')
|
||||
getCompanyLibrary()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -49,6 +57,7 @@ import { ElTable, ElTableColumn, ElButton, ElPagination } from 'element-plus'
|
||||
import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VChatStartupLog'
|
||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import { formatCompanyScale } from '@geekgeekrun/sqlite-plugin/src/utils/parser'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VChatStartupLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -64,6 +73,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getCompanyLibrary() {
|
||||
try {
|
||||
gtagRenderer('company_library_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-company-library', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -75,7 +88,16 @@ async function getCompanyLibrary() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('company_library_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('company_library_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
@@ -0,0 +1,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>
|
||||
@@ -55,7 +55,17 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getJobLibrary">刷新</el-button>
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('job_library_refresh_clicked')
|
||||
getJobLibrary()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.pageNo"
|
||||
@@ -74,7 +84,13 @@
|
||||
<JobInfoSnapshot
|
||||
v-if="selectedJobInfoForViewSnapshot"
|
||||
:job-info="selectedJobInfoForViewSnapshot"
|
||||
@closed="selectedJobInfoForViewSnapshot = null"
|
||||
scene="jobLibrary"
|
||||
@closed="
|
||||
() => {
|
||||
gtagRenderer('job_info_snapshot_closed')
|
||||
selectedJobInfoForViewSnapshot = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ElDrawer>
|
||||
<ElDialog
|
||||
@@ -92,6 +108,7 @@
|
||||
:job-info-history-list="selectedJobHistory ?? []"
|
||||
@closed="
|
||||
() => {
|
||||
gtagRenderer('job_library_list_dialog_closed')
|
||||
selectedJobInfoForViewHistory = null
|
||||
selectedJobHistory = null
|
||||
}
|
||||
@@ -109,6 +126,7 @@ import { type JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/src/entity/Job
|
||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||
import JobInfoHistoryList from '../../features/JobInfoHistoryList/index.vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VChatStartupLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -121,6 +139,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getJobLibrary() {
|
||||
try {
|
||||
gtagRenderer('job_library_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-job-library', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -132,7 +154,16 @@ async function getJobLibrary() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('job_library_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('job_library_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
@@ -160,10 +191,12 @@ const drawVisibleModelValue = ref(false)
|
||||
const selectedJobInfoForViewSnapshot = ref<VChatStartupLog | null>(null)
|
||||
|
||||
function handleViewJobSnapshotButtonClick(record: VChatStartupLog) {
|
||||
gtagRenderer('view_job_snapshot_button_clicked')
|
||||
selectedJobInfoForViewSnapshot.value = record
|
||||
drawVisibleModelValue.value = true
|
||||
}
|
||||
async function handleViewJobOnlineButtonClick(encryptJobId: string) {
|
||||
gtagRenderer('view_job_online_button_clicked')
|
||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||
url: `https://www.zhipin.com/job_detail/${encryptJobId}.html`
|
||||
})
|
||||
@@ -173,6 +206,7 @@ const historyDialogVisibleModelValue = ref(false)
|
||||
const selectedJobInfoForViewHistory = ref<VChatStartupLog | null>(null)
|
||||
const selectedJobHistory = ref<null | JobInfoChangeLog[]>(null)
|
||||
async function handleViewJobHistoryButtonClick(record: VChatStartupLog) {
|
||||
gtagRenderer('view_job_history_button_clicked')
|
||||
let { data: historyList } = await electron.ipcRenderer.invoke(
|
||||
'get-job-history-by-encrypt-id',
|
||||
record.encryptJobId
|
||||
@@ -227,12 +261,13 @@ async function handleViewJobHistoryButtonClick(record: VChatStartupLog) {
|
||||
// })
|
||||
|
||||
if (!historyList.length) {
|
||||
gtagRenderer('job_history_is_not_found')
|
||||
ElMessage.warning({
|
||||
message: '未找到与此条目相关的历史变更记录,再多投一投吧'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
gtagRenderer('job_history_is_found')
|
||||
historyDialogVisibleModelValue.value = true
|
||||
selectedJobInfoForViewHistory.value = record
|
||||
selectedJobHistory.value = historyList
|
||||
@@ -72,7 +72,15 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getMarkAsNotSuitRecord"
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('mansr_refresh_clicked')
|
||||
getMarkAsNotSuitRecord()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -93,7 +101,13 @@
|
||||
<JobInfoSnapshot
|
||||
v-if="selectedJobInfoForViewSnapshot"
|
||||
:job-info="selectedJobInfoForViewSnapshot"
|
||||
@closed="selectedJobInfoForViewSnapshot = null"
|
||||
scene="markAsNotSuitRecord"
|
||||
@closed="
|
||||
() => {
|
||||
gtagRenderer('mansr_closed')
|
||||
selectedJobInfoForViewSnapshot = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
@@ -107,6 +121,7 @@ import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/src/enums'
|
||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VMarkAsNotSuitLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -122,6 +137,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getMarkAsNotSuitRecord() {
|
||||
try {
|
||||
gtagRenderer('mansr_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-mark-as-not-suit-record', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -133,7 +152,16 @@ async function getMarkAsNotSuitRecord() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('mansr_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('mansr_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
@@ -158,6 +186,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
async function handleViewJobOnlineButtonClick(encryptJobId: string) {
|
||||
gtagRenderer('view_job_online_button_clicked')
|
||||
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
|
||||
url: `https://www.zhipin.com/job_detail/${encryptJobId}.html`
|
||||
})
|
||||
@@ -167,6 +196,7 @@ const drawVisibleModelValue = ref(false)
|
||||
const selectedJobInfoForViewSnapshot = ref<VMarkAsNotSuitLog | null>(null)
|
||||
|
||||
function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) {
|
||||
gtagRenderer('view_job_snapshot_button_clicked')
|
||||
selectedJobInfoForViewSnapshot.value = record
|
||||
drawVisibleModelValue.value = true
|
||||
}
|
||||
@@ -0,0 +1,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>
|
||||
@@ -55,7 +55,15 @@
|
||||
</div>
|
||||
<div class="flex flex-0 flex-justify-between pt10px pb10px">
|
||||
<div class="w100px">
|
||||
<el-button :loading="isTableLoading" size="small" @click="getAutoStartChatRecord"
|
||||
<el-button
|
||||
:loading="isTableLoading"
|
||||
size="small"
|
||||
@click="
|
||||
() => {
|
||||
gtagRenderer('start_chat_record_refresh_clicked')
|
||||
getAutoStartChatRecord()
|
||||
}
|
||||
"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -76,7 +84,13 @@
|
||||
<JobInfoSnapshot
|
||||
v-if="selectedJobInfoForViewSnapshot"
|
||||
:job-info="selectedJobInfoForViewSnapshot"
|
||||
@closed="selectedJobInfoForViewSnapshot = null"
|
||||
scene="startChatRecord"
|
||||
@closed="
|
||||
() => {
|
||||
gtagRenderer('start_chat_record_closed')
|
||||
selectedJobInfoForViewSnapshot = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
@@ -89,6 +103,7 @@ import { type VChatStartupLog } from '@geekgeekrun/sqlite-plugin/src/entity/VCha
|
||||
import { transformUtcDateToLocalDate } from '@geekgeekrun/utils/date.mjs'
|
||||
import { PageReq, PagedRes } from '../../../../common/types/pagination'
|
||||
import JobInfoSnapshot from '../../features/JobInfoSnapshot/index.vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const tableData = ref<VChatStartupLog[]>([])
|
||||
const pageSizeList = ref<number[]>([100, 200, 300, 400])
|
||||
@@ -104,6 +119,10 @@ const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
const isTableLoading = ref(false)
|
||||
async function getAutoStartChatRecord() {
|
||||
try {
|
||||
gtagRenderer('start_chat_record_request_sent', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
isTableLoading.value = true
|
||||
const { data: res } = (await electron.ipcRenderer.invoke('get-auto-start-chat-record', {
|
||||
pageNo: pagination.value.pageNo,
|
||||
@@ -115,7 +134,16 @@ async function getAutoStartChatRecord() {
|
||||
pageNo: res.pageNo,
|
||||
pageSize: pagination.value.pageSize
|
||||
}
|
||||
gtagRenderer('start_chat_record_request_success', {
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
} catch (err) {
|
||||
gtagRenderer('start_chat_record_request_error', {
|
||||
err,
|
||||
page_no: pagination.value.pageNo,
|
||||
page_size: pagination.value.pageSize,
|
||||
})
|
||||
console.log(err)
|
||||
tableData.value = []
|
||||
} finally {
|
||||
215
packages/ui/src/renderer/src/page/MainLayout/index.vue
Normal file
215
packages/ui/src/renderer/src/page/MainLayout/index.vue
Normal file
@@ -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>
|
||||
564
packages/ui/src/renderer/src/page/ResumeEditor/index.vue
Normal file
564
packages/ui/src/renderer/src/page/ResumeEditor/index.vue
Normal file
@@ -0,0 +1,564 @@
|
||||
<template>
|
||||
<div class="resume-editor-page">
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
<div class="mt1em mb1em flex flex-items-center flex-justify-between">
|
||||
<span>简历编辑器</span>
|
||||
</div>
|
||||
<el-alert type="info" :closable="false" mb20px line-height-1.25em>
|
||||
<ul pl16px m0>
|
||||
<li>
|
||||
此简历将作为提示词的一部分提交给语言大模型,仅在匹配职位、生成已读不回提醒消息时使用;大部分信息非必填,但在不填写的情况下,可能会匹配到不准确的职位或生成预料之外的已读不回提醒消息
|
||||
</li>
|
||||
<li>期望薪资仅作匹配职位使用,不会用作生成已读不回提醒消息</li>
|
||||
</ul>
|
||||
</el-alert>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formContentForElForm"
|
||||
:rules="formRulesForElForm"
|
||||
label-position="top"
|
||||
class="resume-editor-form"
|
||||
:validate-on-rule-change="false"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '10px'
|
||||
}"
|
||||
>
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="formContent.name" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="工作年限">
|
||||
<el-input v-model="formContent.workYearDesc" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="期望职位">
|
||||
<el-input v-model="formContent.expectJob" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="期望薪资(k)">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-input v-model="formContent.expectSalary[0]" placeholder="下限" />
|
||||
<el-input v-model="formContent.expectSalary[1]" placeholder="上限" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item label="个人优势">
|
||||
<el-input
|
||||
v-model="formContent.userDescription"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="el-form-item__label">
|
||||
工作经历
|
||||
<el-button size="small" :icon="Plus" @click="addWorkExp">新增一条</el-button>
|
||||
</div>
|
||||
<div v-for="(exp, index) in formContent.geekWorkExpList" :key="index">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
gap: '12px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
height: 'fit-content',
|
||||
marginTop: '10px'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveWorkExpUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= formContent.geekWorkExpList.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveWorkExpDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="1 >= formContent.geekWorkExpList.length"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeWorkExp(index)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1.25fr 1fr',
|
||||
gap: '10px',
|
||||
width: '100%'
|
||||
}"
|
||||
>
|
||||
<el-form-item
|
||||
label="公司名称"
|
||||
style="margin-bottom: 18px"
|
||||
:prop="`geekWorkExpList_${index}_company`"
|
||||
>
|
||||
<el-input v-model="exp.company" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任职时间" style="margin-bottom: 18px">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="exp.startYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="开始月份"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="exp.endYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="结束月份"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="职务" style="margin-bottom: 18px">
|
||||
<el-input v-model="exp.positionName" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="工作描述" style="margin-bottom: 18px">
|
||||
<el-input
|
||||
v-model="exp.workDescription"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工作业绩">
|
||||
<el-input
|
||||
v-model="exp.performance"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
<div
|
||||
v-if="index !== formContent.geekWorkExpList.length - 1"
|
||||
class="mt20px mb20px h1px"
|
||||
style="background-color: #dcdcdc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="el-form-item__label">
|
||||
项目经历
|
||||
<el-button size="small" :icon="Plus" @click="addProjExp">新增一条</el-button>
|
||||
</div>
|
||||
<div v-for="(proj, index) in formContent.geekProjExpList" :key="index">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
gap: '12px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
height: 'fit-content',
|
||||
marginTop: '10px'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveProjExpUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= formContent.geekProjExpList.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveProjExpDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="1 >= formContent.geekProjExpList.length"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeProjExp(index)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1.25fr',
|
||||
gap: '10px',
|
||||
width: '100%'
|
||||
}"
|
||||
>
|
||||
<el-form-item
|
||||
label="项目名称"
|
||||
style="margin-bottom: 18px"
|
||||
:prop="`geekProjExpList_${index}_name`"
|
||||
>
|
||||
<el-input v-model="proj.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目角色" style="margin-bottom: 18px">
|
||||
<el-input v-model="proj.roleName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目时间" style="margin-bottom: 18px">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="proj.startYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="开始月份"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="proj.endYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="结束月份"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="项目描述" style="margin-bottom: 18px">
|
||||
<el-input
|
||||
v-model="proj.projectDescription"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目业绩">
|
||||
<el-input
|
||||
v-model="proj.performance"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
<div
|
||||
v-if="index !== formContent.geekProjExpList.length - 1"
|
||||
class="mt20px mb20px h1px"
|
||||
style="background-color: #dcdcdc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</main>
|
||||
</div>
|
||||
<footer pt10px pb10px flex flex-justify-center>
|
||||
<div w768px flex flex-justify-between>
|
||||
<div>
|
||||
<!-- <el-button type="text" @click="handleTestAvailability">测试可用性</el-button> -->
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElForm, ElButton, ElAlert, ElMessageBox } from 'element-plus'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { type ResumeContent, resumeContentEnoughDetect } from '../../../../common/utils/resume'
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
|
||||
const getEmptyFormContent = () => {
|
||||
const o: any = {
|
||||
expectJob: '',
|
||||
name: '',
|
||||
userDescription: '',
|
||||
workYearDesc: '',
|
||||
expectSalary: ['', ''],
|
||||
geekWorkExpList: [],
|
||||
geekProjExpList: []
|
||||
}
|
||||
o.geekProjExpList = [getNewProjExpItem()]
|
||||
o.geekWorkExpList = [getNewWorkExpItem()]
|
||||
|
||||
return o as ResumeContent
|
||||
}
|
||||
const formContent = ref<ResumeContent>(getEmptyFormContent())
|
||||
|
||||
const formContentForElForm = computed(() => {
|
||||
const valueMap = {}
|
||||
formContent.value.geekWorkExpList?.forEach((item, i) => {
|
||||
valueMap[`geekWorkExpList_${i}_company`] = item.company
|
||||
})
|
||||
formContent.value.geekProjExpList?.forEach((item, i) => {
|
||||
valueMap[`geekProjExpList_${i}_name`] = item.name
|
||||
})
|
||||
return valueMap
|
||||
})
|
||||
const formRulesForElForm = computed(() => {
|
||||
const valueMap = {}
|
||||
formContent.value.geekWorkExpList.forEach((_, i) => {
|
||||
valueMap[`geekWorkExpList_${i}_company`] = [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入公司名称'
|
||||
},
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator(_, value, cb) {
|
||||
if (!value.trim()) {
|
||||
cb(`请输入公司名称`)
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
formContent.value.geekProjExpList.forEach((_, i) => {
|
||||
valueMap[`geekProjExpList_${i}_name`] = [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入项目名称'
|
||||
},
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator(_, value, cb) {
|
||||
if (!value.trim()) {
|
||||
cb(`请输入项目名称`)
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
return valueMap
|
||||
})
|
||||
const handleCancel = () => {
|
||||
gtagRenderer('cancel_clicked')
|
||||
electron.ipcRenderer.send('close-resume-editor')
|
||||
gtagRenderer('cancel_done')
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
gtagRenderer('submit_clicked')
|
||||
if (
|
||||
!resumeContentEnoughDetect({
|
||||
content: formContent.value
|
||||
})
|
||||
) {
|
||||
try {
|
||||
gtagRenderer('rc_not_enough_dialog_show')
|
||||
await ElMessageBox.confirm(
|
||||
`简历内容可能不够充足(各个部分内容长度相加 <800 字)<br />后续大模型根据简历生成的内容将可能不符合预期(例如相同内容重复生成、生成预期之外的内容)<br /><br />要继续保存吗?`,
|
||||
{
|
||||
cancelButtonText: '不,我再改改',
|
||||
confirmButtonText: '是的,继续保存',
|
||||
dangerouslyUseHTMLString: true
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
electron.ipcRenderer.invoke('save-resume-content', JSON.parse(JSON.stringify(formContent.value)))
|
||||
gtagRenderer('submit_done')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const savedFileContent = await electron.ipcRenderer.invoke('fetch-resume-content')
|
||||
if (!savedFileContent) {
|
||||
return
|
||||
}
|
||||
for (const k of Object.keys(formContent.value)) {
|
||||
formContent.value[k] = savedFileContent[k]
|
||||
}
|
||||
if (!formContent.value.expectSalary) {
|
||||
formContent.value.expectSalary = ['', '']
|
||||
}
|
||||
if (!formContent.value.expectSalary?.[0] || /\D/.test(formContent.value.expectSalary?.[0])) {
|
||||
formContent.value.expectSalary[0] = ''
|
||||
}
|
||||
if (!formContent.value.expectSalary?.[1] || /\D/.test(formContent.value.expectSalary?.[1])) {
|
||||
formContent.value.expectSalary[1] = ''
|
||||
}
|
||||
if (!formContent.value.geekProjExpList?.length) {
|
||||
formContent.value.geekProjExpList = [getNewProjExpItem()]
|
||||
}
|
||||
if (!formContent.value.geekWorkExpList?.length) {
|
||||
formContent.value.geekWorkExpList = [getNewWorkExpItem()]
|
||||
}
|
||||
} catch (err) {
|
||||
formContent.value = getEmptyFormContent()
|
||||
}
|
||||
})
|
||||
|
||||
// function handlePresetClick(selected: (typeof llmPresetList)[number]) {}
|
||||
|
||||
// #region edit work exp list
|
||||
function getNewWorkExpItem() {
|
||||
return {
|
||||
company: '',
|
||||
endYearMon: '',
|
||||
positionName: '',
|
||||
startYearMon: '',
|
||||
performance: '',
|
||||
workDescription: ''
|
||||
}
|
||||
}
|
||||
function addWorkExp() {
|
||||
formContent.value.geekWorkExpList.push(getNewWorkExpItem())
|
||||
gtagRenderer('resume_work_exp_added')
|
||||
}
|
||||
function moveWorkExpUp(index) {
|
||||
;[formContent.value.geekWorkExpList[index], formContent.value.geekWorkExpList[index - 1]] = [
|
||||
formContent.value.geekWorkExpList[index - 1],
|
||||
formContent.value.geekWorkExpList[index]
|
||||
]
|
||||
gtagRenderer('resume_work_exp_moved_up')
|
||||
}
|
||||
|
||||
function moveWorkExpDown(index) {
|
||||
;[formContent.value.geekWorkExpList[index], formContent.value.geekWorkExpList[index + 1]] = [
|
||||
formContent.value.geekWorkExpList[index + 1],
|
||||
formContent.value.geekWorkExpList[index]
|
||||
]
|
||||
gtagRenderer('resume_work_exp_moved_down')
|
||||
}
|
||||
|
||||
function removeWorkExp(index) {
|
||||
formContent.value.geekWorkExpList.splice(index, 1)
|
||||
gtagRenderer('resume_work_exp_removed')
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region edit proj list
|
||||
function getNewProjExpItem() {
|
||||
return {
|
||||
name: '',
|
||||
endYearMon: '',
|
||||
roleName: '',
|
||||
startYearMon: '',
|
||||
performance: '',
|
||||
projectDescription: ''
|
||||
}
|
||||
}
|
||||
function addProjExp() {
|
||||
formContent.value.geekProjExpList.push(getNewProjExpItem())
|
||||
gtagRenderer('resume_proj_exp_added')
|
||||
}
|
||||
function moveProjExpUp(index) {
|
||||
;[formContent.value.geekProjExpList[index], formContent.value.geekProjExpList[index - 1]] = [
|
||||
formContent.value.geekProjExpList[index - 1],
|
||||
formContent.value.geekProjExpList[index]
|
||||
]
|
||||
gtagRenderer('resume_proj_exp_moved_up')
|
||||
}
|
||||
|
||||
function moveProjExpDown(index) {
|
||||
;[formContent.value.geekProjExpList[index], formContent.value.geekProjExpList[index + 1]] = [
|
||||
formContent.value.geekProjExpList[index + 1],
|
||||
formContent.value.geekProjExpList[index]
|
||||
]
|
||||
gtagRenderer('resume_proj_exp_moved_down')
|
||||
}
|
||||
|
||||
function removeProjExp(index) {
|
||||
formContent.value.geekProjExpList.splice(index, 1)
|
||||
gtagRenderer('resume_proj_exp_removed')
|
||||
}
|
||||
// #endregion
|
||||
|
||||
onMounted(() => {
|
||||
gtagRenderer('resume_editor_mounted')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.resume-editor-page {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
.main-wrapper {
|
||||
overflow: auto;
|
||||
main {
|
||||
margin: 0 auto;
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
footer {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.resume-editor-form.el-form {
|
||||
.el-form-item__error--inline {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
import BootstrapSplash from '@renderer/page/BootstrapSplash/index.vue'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
@@ -17,55 +18,76 @@ const routes: Array<RouteRecordRaw> = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/configuration',
|
||||
component: () => import('@renderer/page/Configuration/index.vue'),
|
||||
redirect: '/configuration/GeekAutoStartChatWithBoss',
|
||||
path: '/llmConfig',
|
||||
component: () => import('@renderer/page/LlmConfig/index.vue'),
|
||||
meta: {
|
||||
title: '大语言模型设置'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/resumeEditor',
|
||||
component: () => import('@renderer/page/ResumeEditor/index.vue'),
|
||||
meta: {
|
||||
title: '简历编辑'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/readNoReplyReminderLlmMock',
|
||||
component: () => import('@renderer/page/ReadNoReplyReminderLlmMock/index.vue'),
|
||||
meta: {
|
||||
title: '已读不回提醒器 大语言模型测试'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/main-layout',
|
||||
component: () => import('@renderer/page/MainLayout/index.vue'),
|
||||
redirect: '/main-layout/GeekAutoStartChatWithBoss',
|
||||
children: [
|
||||
{
|
||||
path: 'GeekAutoStartChatWithBoss',
|
||||
component: () => import('@renderer/page/Configuration/GeekAutoStartChatWithBoss.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss.vue'),
|
||||
meta: {
|
||||
title: 'BOSS炸弹'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ReadNoReplyReminder',
|
||||
component: () => import('@renderer/page/Configuration/ReadNoReplyReminder.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/ReadNoReplyReminder.vue'),
|
||||
meta: {
|
||||
title: '已读不回提醒器'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'StartChatRecord',
|
||||
component: () => import('@renderer/page/Configuration/StartChatRecord.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/StartChatRecord.vue'),
|
||||
meta: {
|
||||
title: '开聊记录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'MarkAsNotSuitRecord',
|
||||
component: () => import('@renderer/page/Configuration/MarkAsNotSuitRecord.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/MarkAsNotSuitRecord.vue'),
|
||||
meta: {
|
||||
title: '标记不合适记录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'JobLibrary',
|
||||
component: () => import('@renderer/page/Configuration/JobLibrary.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/JobLibrary.vue'),
|
||||
meta: {
|
||||
title: '职位库'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'BossLibrary',
|
||||
component: () => import('@renderer/page/Configuration/BossLibrary.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/BossLibrary.vue'),
|
||||
meta: {
|
||||
title: 'Boss库'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'CompanyLibrary',
|
||||
component: () => import('@renderer/page/Configuration/CompanyLibrary.vue'),
|
||||
component: () => import('@renderer/page/MainLayout/CompanyLibrary.vue'),
|
||||
meta: {
|
||||
title: '公司库'
|
||||
}
|
||||
@@ -114,7 +136,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import('@renderer/page/BootstrapSplash/page/DownloadingDependencies.vue'),
|
||||
meta: {
|
||||
title: '正在下载核心组件'
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -125,12 +147,16 @@ const router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
router.afterEach((to, from) => {
|
||||
if (to.meta?.title) {
|
||||
document.title = `${to.meta.title} - GeekGeekRun`
|
||||
document.title = `${to.meta.title} - GeekGeekRun 牛人快跑`
|
||||
} else {
|
||||
document.title = `GeekGeekRun`
|
||||
document.title = `GeekGeekRun 牛人快跑`
|
||||
}
|
||||
gtagRenderer('router_path_changed', {
|
||||
from_path: from.fullPath,
|
||||
to_path: to.fullPath
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
17
packages/ui/src/renderer/src/utils/gtag.ts
Normal file
17
packages/ui/src/renderer/src/utils/gtag.ts
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
26
packages/utils/gpt-request.mjs
Normal file
26
packages/utils/gpt-request.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
import OpenAI from "openai";
|
||||
|
||||
export async function completes(
|
||||
{
|
||||
baseURL,
|
||||
apiKey,
|
||||
model
|
||||
},
|
||||
messages
|
||||
) {
|
||||
const openai = new OpenAI({
|
||||
baseURL,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
messages,
|
||||
model,
|
||||
frequency_penalty: 0,
|
||||
max_tokens: 100,
|
||||
temperature: 0.1
|
||||
});
|
||||
|
||||
console.log(completion.choices[0].message.content);
|
||||
return completion;
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "utils",
|
||||
"scripts": {
|
||||
},
|
||||
"scripts": {},
|
||||
"author": "geekgeekrun",
|
||||
"license": "ISC",
|
||||
"dependencies": {}
|
||||
"dependencies": {
|
||||
"openai": "^4.91.1"
|
||||
}
|
||||
}
|
||||
|
||||
88
pnpm-lock.yaml
generated
88
pnpm-lock.yaml
generated
@@ -141,6 +141,9 @@ importers:
|
||||
puppeteer-extra-plugin-stealth:
|
||||
specifier: 2.11.2
|
||||
version: 2.11.2(puppeteer-extra@3.3.6)
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
devDependencies:
|
||||
'@electron-toolkit/eslint-config':
|
||||
specifier: ^1.0.2
|
||||
@@ -226,6 +229,9 @@ importers:
|
||||
sass:
|
||||
specifier: ^1.70.0
|
||||
version: 1.70.0
|
||||
spark-md5:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
terser:
|
||||
specifier: ^5.37.0
|
||||
version: 5.37.0
|
||||
@@ -257,7 +263,11 @@ importers:
|
||||
specifier: ^1.8.27
|
||||
version: 1.8.27(typescript@5.3.3)
|
||||
|
||||
packages/utils: {}
|
||||
packages/utils:
|
||||
dependencies:
|
||||
openai:
|
||||
specifier: ^4.91.1
|
||||
version: 4.91.1
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1679,6 +1689,13 @@ packages:
|
||||
/@types/ms@0.7.34:
|
||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||
|
||||
/@types/node-fetch@2.6.12:
|
||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||
dependencies:
|
||||
'@types/node': 18.19.15
|
||||
form-data: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@types/node@18.19.15:
|
||||
resolution: {integrity: sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==}
|
||||
dependencies:
|
||||
@@ -2302,6 +2319,13 @@ packages:
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
|
||||
/abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
dev: false
|
||||
|
||||
/acorn-jsx@5.3.2(acorn@8.11.3):
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -2349,7 +2373,6 @@ packages:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/aggregate-error@3.1.0:
|
||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||
@@ -2546,7 +2569,6 @@ packages:
|
||||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: true
|
||||
|
||||
/at-least-node@1.0.0:
|
||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||
@@ -2904,7 +2926,6 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: true
|
||||
|
||||
/commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
@@ -3108,7 +3129,6 @@ packages:
|
||||
/delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/delegates@1.0.0:
|
||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||
@@ -3607,6 +3627,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/execa@5.1.1:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3776,6 +3801,10 @@ packages:
|
||||
signal-exit: 4.1.0
|
||||
dev: true
|
||||
|
||||
/form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
dev: false
|
||||
|
||||
/form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3783,7 +3812,14 @@ packages:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/formdata-node@4.4.1:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
dev: false
|
||||
|
||||
/formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
@@ -4162,7 +4198,6 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/iconv-corefoundation@1.1.7:
|
||||
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
||||
@@ -4643,14 +4678,12 @@ packages:
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: true
|
||||
|
||||
/mime@2.6.0:
|
||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||
@@ -5002,6 +5035,29 @@ packages:
|
||||
mimic-fn: 2.1.0
|
||||
dev: true
|
||||
|
||||
/openai@4.91.1:
|
||||
resolution: {integrity: sha512-DbjrR0hIMQFbxz8+3qBsfPJnh3+I/skPgoSlT7f9eiZuhGBUissPQULNgx6gHNkLoZ3uS0uYS6eXPUdtg4nHzw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.19.15
|
||||
'@types/node-fetch': 2.6.12
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.5.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.6.7
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/optionator@0.9.3:
|
||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5726,6 +5782,10 @@ packages:
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
/spark-md5@3.0.2:
|
||||
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||
dev: true
|
||||
|
||||
/sprintf-js@1.1.3:
|
||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||
requiresBuild: true
|
||||
@@ -6360,6 +6420,11 @@ packages:
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
/uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
@@ -6499,6 +6564,11 @@ packages:
|
||||
engines: {node: '>= 8'}
|
||||
dev: false
|
||||
|
||||
/web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
|
||||
Reference in New Issue
Block a user