diff --git a/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json b/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json index 435e5ca..ee0f425 100644 --- a/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json +++ b/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json @@ -9,6 +9,10 @@ "expectJobRegExpStr": "", "autoReminder": { "throttleIntervalMinutes": 10, - "rechatLimitDay": 21 + "rechatLimitDay": 21, + "geminiApiKey": "", + "rechatContentSource": 1, + "recentMessageQuantityForLlm": 8, + "rechatLlmFallback": 1 } } \ No newline at end of file diff --git a/packages/geek-auto-start-chat-with-boss/default-config-file/llm.json b/packages/geek-auto-start-chat-with-boss/default-config-file/llm.json new file mode 100644 index 0000000..87389d9 --- /dev/null +++ b/packages/geek-auto-start-chat-with-boss/default-config-file/llm.json @@ -0,0 +1,8 @@ +[{ + "providerCompleteApiUrl": "", + "providerApiSecret": "", + "model": "", + "enabled": true, + "serveWeight": 100, + "_extra": {} +}] \ No newline at end of file diff --git a/packages/geek-auto-start-chat-with-boss/default-config-file/target-company-list.json b/packages/geek-auto-start-chat-with-boss/default-config-file/target-company-list.json index e9fb758..a5966ba 100644 --- a/packages/geek-auto-start-chat-with-boss/default-config-file/target-company-list.json +++ b/packages/geek-auto-start-chat-with-boss/default-config-file/target-company-list.json @@ -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","稀宇","深言","百川智能","与爱为舞","牵手" ] \ No newline at end of file diff --git a/packages/geek-auto-start-chat-with-boss/index.mjs b/packages/geek-auto-start-chat-with-boss/index.mjs index 79349c3..c358ac4 100644 --- a/packages/geek-auto-start-chat-with-boss/index.mjs +++ b/packages/geek-auto-start-chat-with-boss/index.mjs @@ -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 } } diff --git a/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs b/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs index 091345c..dd9512a 100644 --- a/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs +++ b/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs @@ -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 diff --git a/packages/launch-bosszhipin-login-page-with-preload-extension/extensions/EditThisCookie.zip b/packages/launch-bosszhipin-login-page-with-preload-extension/extensions/EditThisCookie.zip index d817ee9..3f8724d 100644 Binary files a/packages/launch-bosszhipin-login-page-with-preload-extension/extensions/EditThisCookie.zip and b/packages/launch-bosszhipin-login-page-with-preload-extension/extensions/EditThisCookie.zip differ diff --git a/packages/launch-bosszhipin-login-page-with-preload-extension/index.mjs b/packages/launch-bosszhipin-login-page-with-preload-extension/index.mjs index 7bad52f..cecd3cd 100644 --- a/packages/launch-bosszhipin-login-page-with-preload-extension/index.mjs +++ b/packages/launch-bosszhipin-login-page-with-preload-extension/index.mjs @@ -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, diff --git a/packages/launch-bosszhipin-login-page-with-preload-extension/utils.mjs b/packages/launch-bosszhipin-login-page-with-preload-extension/utils.mjs new file mode 100644 index 0000000..da1f6f8 --- /dev/null +++ b/packages/launch-bosszhipin-login-page-with-preload-extension/utils.mjs @@ -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 + } + ) + } +} \ No newline at end of file diff --git a/packages/sqlite-plugin/src/entity/ChatMessageRecord.ts b/packages/sqlite-plugin/src/entity/ChatMessageRecord.ts index 3f883bf..fc29e3d 100644 --- a/packages/sqlite-plugin/src/entity/ChatMessageRecord.ts +++ b/packages/sqlite-plugin/src/entity/ChatMessageRecord.ts @@ -25,7 +25,7 @@ export class ChatMessageRecord { @Column({ nullable: true }) - style?: 'sent' | 'receive'; + style?: 'sent' | 'received'; @Column({ nullable: true diff --git a/packages/sqlite-plugin/src/entity/LlmModelUsageRecord.ts b/packages/sqlite-plugin/src/entity/LlmModelUsageRecord.ts new file mode 100644 index 0000000..db5c179 --- /dev/null +++ b/packages/sqlite-plugin/src/entity/LlmModelUsageRecord.ts @@ -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 +} diff --git a/packages/sqlite-plugin/src/handlers.ts b/packages/sqlite-plugin/src/handlers.ts index ec000c3..89bca04 100644 --- a/packages/sqlite-plugin/src/handlers.ts +++ b/packages/sqlite-plugin/src/handlers.ts @@ -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 +} diff --git a/packages/sqlite-plugin/src/index.ts b/packages/sqlite-plugin/src/index.ts index 969fbc1..555b859 100644 --- a/packages/sqlite-plugin/src/index.ts +++ b/packages/sqlite-plugin/src/index.ts @@ -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 }); diff --git a/packages/sqlite-plugin/src/migrations/1732032381304-UpdateBossInfoTable.ts b/packages/sqlite-plugin/src/migrations/1732032381304-UpdateBossInfoTable.ts index 200bda9..1a3a979 100644 --- a/packages/sqlite-plugin/src/migrations/1732032381304-UpdateBossInfoTable.ts +++ b/packages/sqlite-plugin/src/migrations/1732032381304-UpdateBossInfoTable.ts @@ -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 { - 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 { + for (const viewName of viewNames) { + await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`); } - - public async down(queryRunner: QueryRunner): Promise { + 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 {} } diff --git a/packages/ui/package.json b/packages/ui/package.json index ed4cb89..fa23227 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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", diff --git a/packages/ui/src/common/constant.ts b/packages/ui/src/common/constant.ts new file mode 100644 index 0000000..1570f00 --- /dev/null +++ b/packages/ui/src/common/constant.ts @@ -0,0 +1 @@ +export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1 diff --git a/packages/ui/src/common/enums/auto-start-chat.ts b/packages/ui/src/common/enums/auto-start-chat.ts index a0e001a..ff0cc95 100644 --- a/packages/ui/src/common/enums/auto-start-chat.ts +++ b/packages/ui/src/common/enums/auto-start-chat.ts @@ -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 +} diff --git a/packages/ui/src/common/utils/resume.ts b/packages/ui/src/common/utils/resume.ts new file mode 100644 index 0000000..5c24bbb --- /dev/null +++ b/packages/ui/src/common/utils/resume.ts @@ -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 +} diff --git a/packages/ui/src/main/features/llm-request-log.ts b/packages/ui/src/main/features/llm-request-log.ts new file mode 100644 index 0000000..f421b8b --- /dev/null +++ b/packages/ui/src/main/features/llm-request-log.ts @@ -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]) +} diff --git a/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts b/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts index ff97e1c..03eb2e8 100644 --- a/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts +++ b/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts @@ -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') diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts index 5784468..8d2409c 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts @@ -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 () { diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts index 084f189..fe2def6 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts @@ -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) }) diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/utils/db/worker/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/utils/db/worker/index.ts index d8b0c5a..c9d1d93 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/utils/db/worker/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/utils/db/worker/index.ts @@ -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 { diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts index 475115e..5b2227c 100644 --- a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts @@ -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 & { + 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) +} diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts index 063c76a..1d01653 100644 --- a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts @@ -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) diff --git a/packages/ui/src/main/utils/gtag/index.ts b/packages/ui/src/main/utils/gtag/index.ts index 2a1dc9d..ac3a11f 100644 --- a/packages/ui/src/main/utils/gtag/index.ts +++ b/packages/ui/src/main/utils/gtag/index.ts @@ -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( ...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. diff --git a/packages/ui/src/main/utils/initPublicIpc.ts b/packages/ui/src/main/utils/initPublicIpc.ts new file mode 100644 index 0000000..502d7fe --- /dev/null +++ b/packages/ui/src/main/utils/initPublicIpc.ts @@ -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 + } + ) + }) +} diff --git a/packages/ui/src/main/window/firstLaunchNoticeWindow.ts b/packages/ui/src/main/window/firstLaunchNoticeWindow.ts index 9fa0836..626f380 100644 --- a/packages/ui/src/main/window/firstLaunchNoticeWindow.ts +++ b/packages/ui/src/main/window/firstLaunchNoticeWindow.ts @@ -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() diff --git a/packages/ui/src/main/window/llmConfigWindow.ts b/packages/ui/src/main/window/llmConfigWindow.ts new file mode 100644 index 0000000..21eab17 --- /dev/null +++ b/packages/ui/src/main/window/llmConfigWindow.ts @@ -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! +} diff --git a/packages/ui/src/main/window/mainWindow.ts b/packages/ui/src/main/window/mainWindow.ts index 3998213..5d3e60d 100644 --- a/packages/ui/src/main/window/mainWindow.ts +++ b/packages/ui/src/main/window/mainWindow.ts @@ -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) diff --git a/packages/ui/src/main/window/readNoReplyReminderLlmMockWindow.ts b/packages/ui/src/main/window/readNoReplyReminderLlmMockWindow.ts new file mode 100644 index 0000000..d2909bb --- /dev/null +++ b/packages/ui/src/main/window/readNoReplyReminderLlmMockWindow.ts @@ -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! +} diff --git a/packages/ui/src/main/window/resumeEditorWindow.ts b/packages/ui/src/main/window/resumeEditorWindow.ts new file mode 100644 index 0000000..3ccb453 --- /dev/null +++ b/packages/ui/src/main/window/resumeEditorWindow.ts @@ -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! +} diff --git a/packages/ui/src/renderer/src/App.vue b/packages/ui/src/renderer/src/App.vue index cb291fa..e804103 100644 --- a/packages/ui/src/renderer/src/App.vue +++ b/packages/ui/src/renderer/src/App.vue @@ -9,4 +9,11 @@ diff --git a/packages/ui/src/renderer/src/features/JobInfoHistoryList/index.vue b/packages/ui/src/renderer/src/features/JobInfoHistoryList/index.vue index 90d9574..6d58f18 100644 --- a/packages/ui/src/renderer/src/features/JobInfoHistoryList/index.vue +++ b/packages/ui/src/renderer/src/features/JobInfoHistoryList/index.vue @@ -24,7 +24,11 @@ diff --git a/packages/ui/src/renderer/src/page/BootstrapSplash/index.vue b/packages/ui/src/renderer/src/page/BootstrapSplash/index.vue index 90138c0..d100754 100644 --- a/packages/ui/src/renderer/src/page/BootstrapSplash/index.vue +++ b/packages/ui/src/renderer/src/page/BootstrapSplash/index.vue @@ -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') } }) }) diff --git a/packages/ui/src/renderer/src/page/BootstrapSplash/page/DownloadingDependencies.vue b/packages/ui/src/renderer/src/page/BootstrapSplash/page/DownloadingDependencies.vue index fbc663d..5d6fcbf 100644 --- a/packages/ui/src/renderer/src/page/BootstrapSplash/page/DownloadingDependencies.vue +++ b/packages/ui/src/renderer/src/page/BootstrapSplash/page/DownloadingDependencies.vue @@ -13,6 +13,7 @@ - - diff --git a/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue deleted file mode 100644 index efa25f0..0000000 --- a/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue +++ /dev/null @@ -1,176 +0,0 @@ - - - - - diff --git a/packages/ui/src/renderer/src/page/Configuration/index.vue b/packages/ui/src/renderer/src/page/Configuration/index.vue deleted file mode 100644 index d1a257b..0000000 --- a/packages/ui/src/renderer/src/page/Configuration/index.vue +++ /dev/null @@ -1,148 +0,0 @@ - - - - - diff --git a/packages/ui/src/renderer/src/page/CookieAssistant/index.vue b/packages/ui/src/renderer/src/page/CookieAssistant/index.vue index ca259fe..3b8d862 100644 --- a/packages/ui/src/renderer/src/page/CookieAssistant/index.vue +++ b/packages/ui/src/renderer/src/page/CookieAssistant/index.vue @@ -1,6 +1,6 @@ { 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; } diff --git a/packages/ui/src/renderer/src/page/FirstRunReadme/index.vue b/packages/ui/src/renderer/src/page/FirstRunReadme/index.vue index 8b667c2..fa7599d 100644 --- a/packages/ui/src/renderer/src/page/FirstRunReadme/index.vue +++ b/packages/ui/src/renderer/src/page/FirstRunReadme/index.vue @@ -87,6 +87,8 @@ diff --git a/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue index e5da026..7bc40b4 100644 --- a/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue +++ b/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue @@ -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 }) } }) diff --git a/packages/ui/src/renderer/src/page/LlmConfig/index.vue b/packages/ui/src/renderer/src/page/LlmConfig/index.vue new file mode 100644 index 0000000..eaa92e4 --- /dev/null +++ b/packages/ui/src/renderer/src/page/LlmConfig/index.vue @@ -0,0 +1,563 @@ + + + + + + + diff --git a/packages/ui/src/renderer/src/page/Configuration/BossLibrary.vue b/packages/ui/src/renderer/src/page/MainLayout/BossLibrary.vue similarity index 81% rename from packages/ui/src/renderer/src/page/Configuration/BossLibrary.vue rename to packages/ui/src/renderer/src/page/MainLayout/BossLibrary.vue index 5fef83d..b0ccd20 100644 --- a/packages/ui/src/renderer/src/page/Configuration/BossLibrary.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/BossLibrary.vue @@ -19,7 +19,17 @@
- 刷新 + 刷新
([]) const pageSizeList = ref([100, 200, 300, 400]) @@ -54,6 +65,10 @@ const tableRef = ref>() 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 { diff --git a/packages/ui/src/renderer/src/page/Configuration/CompanyLibrary.vue b/packages/ui/src/renderer/src/page/MainLayout/CompanyLibrary.vue similarity index 83% rename from packages/ui/src/renderer/src/page/Configuration/CompanyLibrary.vue rename to packages/ui/src/renderer/src/page/MainLayout/CompanyLibrary.vue index bf40837..9630651 100644 --- a/packages/ui/src/renderer/src/page/Configuration/CompanyLibrary.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/CompanyLibrary.vue @@ -23,7 +23,15 @@
- 刷新
@@ -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([]) const pageSizeList = ref([100, 200, 300, 400]) @@ -64,6 +73,10 @@ const tableRef = ref>() 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 { diff --git a/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue new file mode 100644 index 0000000..e339872 --- /dev/null +++ b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue @@ -0,0 +1,499 @@ + + + + + + + diff --git a/packages/ui/src/renderer/src/page/Configuration/JobLibrary.vue b/packages/ui/src/renderer/src/page/MainLayout/JobLibrary.vue similarity index 91% rename from packages/ui/src/renderer/src/page/Configuration/JobLibrary.vue rename to packages/ui/src/renderer/src/page/MainLayout/JobLibrary.vue index 11622d5..dabff25 100644 --- a/packages/ui/src/renderer/src/page/Configuration/JobLibrary.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/JobLibrary.vue @@ -55,7 +55,17 @@
- 刷新 + 刷新
- 刷新
@@ -93,7 +101,13 @@
@@ -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([]) const pageSizeList = ref([100, 200, 300, 400]) @@ -122,6 +137,10 @@ const tableRef = ref>() 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(null) function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) { + gtagRenderer('view_job_snapshot_button_clicked') selectedJobInfoForViewSnapshot.value = record drawVisibleModelValue.value = true } diff --git a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue new file mode 100644 index 0000000..97203cb --- /dev/null +++ b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue @@ -0,0 +1,500 @@ + + + + + diff --git a/packages/ui/src/renderer/src/page/Configuration/StartChatRecord.vue b/packages/ui/src/renderer/src/page/MainLayout/StartChatRecord.vue similarity index 85% rename from packages/ui/src/renderer/src/page/Configuration/StartChatRecord.vue rename to packages/ui/src/renderer/src/page/MainLayout/StartChatRecord.vue index 978d41d..7d947d7 100644 --- a/packages/ui/src/renderer/src/page/Configuration/StartChatRecord.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/StartChatRecord.vue @@ -55,7 +55,15 @@
- 刷新
@@ -76,7 +84,13 @@
@@ -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([]) const pageSizeList = ref([100, 200, 300, 400]) @@ -104,6 +119,10 @@ const tableRef = ref>() 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 { diff --git a/packages/ui/src/renderer/src/page/MainLayout/index.vue b/packages/ui/src/renderer/src/page/MainLayout/index.vue new file mode 100644 index 0000000..ff3e210 --- /dev/null +++ b/packages/ui/src/renderer/src/page/MainLayout/index.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/packages/ui/src/renderer/src/page/MainLayout/resources/intro-of-job-entry.png b/packages/ui/src/renderer/src/page/MainLayout/resources/intro-of-job-entry.png new file mode 100644 index 0000000..33be529 Binary files /dev/null and b/packages/ui/src/renderer/src/page/MainLayout/resources/intro-of-job-entry.png differ diff --git a/packages/ui/src/renderer/src/page/MainLayout/resources/intro-of-job-info.png b/packages/ui/src/renderer/src/page/MainLayout/resources/intro-of-job-info.png new file mode 100644 index 0000000..6844730 Binary files /dev/null and b/packages/ui/src/renderer/src/page/MainLayout/resources/intro-of-job-info.png differ diff --git a/packages/ui/src/renderer/src/page/MainLayout/resources/look-forward-reply-emotion.gif b/packages/ui/src/renderer/src/page/MainLayout/resources/look-forward-reply-emotion.gif new file mode 100644 index 0000000..ee6db58 Binary files /dev/null and b/packages/ui/src/renderer/src/page/MainLayout/resources/look-forward-reply-emotion.gif differ diff --git a/packages/ui/src/renderer/src/page/ReadNoReplyReminderLlmMock/index.vue b/packages/ui/src/renderer/src/page/ReadNoReplyReminderLlmMock/index.vue new file mode 100644 index 0000000..cd5a59f --- /dev/null +++ b/packages/ui/src/renderer/src/page/ReadNoReplyReminderLlmMock/index.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/packages/ui/src/renderer/src/page/ResumeEditor/index.vue b/packages/ui/src/renderer/src/page/ResumeEditor/index.vue new file mode 100644 index 0000000..4f55189 --- /dev/null +++ b/packages/ui/src/renderer/src/page/ResumeEditor/index.vue @@ -0,0 +1,564 @@ + + + + + + + diff --git a/packages/ui/src/renderer/src/router/index.ts b/packages/ui/src/renderer/src/router/index.ts index f665473..6a45aea 100644 --- a/packages/ui/src/renderer/src/router/index.ts +++ b/packages/ui/src/renderer/src/router/index.ts @@ -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 = [ { @@ -17,55 +18,76 @@ const routes: Array = [ } }, { - 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 = [ 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 diff --git a/packages/ui/src/renderer/src/utils/gtag.ts b/packages/ui/src/renderer/src/utils/gtag.ts new file mode 100644 index 0000000..47f27ab --- /dev/null +++ b/packages/ui/src/renderer/src/utils/gtag.ts @@ -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) + } +} diff --git a/packages/utils/gpt-request.mjs b/packages/utils/gpt-request.mjs new file mode 100644 index 0000000..deec331 --- /dev/null +++ b/packages/utils/gpt-request.mjs @@ -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; +} \ No newline at end of file diff --git a/packages/utils/package.json b/packages/utils/package.json index 8c1e536..b6a5017 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -3,9 +3,10 @@ "private": true, "version": "1.0.0", "description": "utils", - "scripts": { - }, + "scripts": {}, "author": "geekgeekrun", "license": "ISC", - "dependencies": {} + "dependencies": { + "openai": "^4.91.1" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9243387..d370d0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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