Compare commits

...

49 Commits

Author SHA1 Message Date
geekgeekrun
4048e3b323 ui-v0.17.1 2026-02-27 10:30:30 +08:00
geekgeekrun
e6489db6c4 Merge branch 'feature/ui' 2026-02-27 10:30:09 +08:00
geekgeekrun
3f08481657 fix style of user agreement 2026-02-27 10:29:46 +08:00
geekgeekrun
97c0f3dfaf ui-v0.17.0 2026-02-27 01:03:58 +08:00
geekgeekrun
80158682b0 Merge branch 'feature/ui' 2026-02-27 01:03:06 +08:00
geekgeekrun
bf1d9023a6 Merge commit 'fff8ca7dff95454ffba05566c5252a1625cf1bc2' 2026-02-27 01:02:49 +08:00
geekgeekrun
b7ffe30ee0 enhance style of first run readme window 2026-02-27 00:47:46 +08:00
geekgeekrun
f82987bd76 add frame for almost window 2026-02-27 00:47:15 +08:00
geekgeekrun
57bab1900c enhance gtag 2026-02-27 00:13:57 +08:00
geekgeekrun
1c7d3d992a add feature for customize open message 2026-02-26 23:14:38 +08:00
geekgeekrun
fff8ca7dff ui-v0.16.0 2026-02-22 14:57:05 +08:00
geekgeekrun
98ffc3a143 Merge branch 'feature/support-other-platform-and-add-global-setting' into feature/ui 2026-02-22 14:54:25 +08:00
geekgeekrun
70d6453e61 enhance ui text 2026-02-22 14:54:02 +08:00
geekgeekrun
679090c9af update README 2026-02-22 14:35:17 +08:00
geekgeekrun
fb7b545d10 remove debugger 2026-02-22 14:28:25 +08:00
geekgeekrun
88c3d63cb6 Merge remote-tracking branch 'origin/master' into feature/support-other-platform-and-add-global-setting 2026-02-22 14:24:08 +08:00
geekgeekrun
40328a24ef update README 2026-02-22 14:16:49 +08:00
geekgeekrun
b1282c8169 don't init props in fieldsForUseCommonConfig with true in boss.json 2026-02-22 13:29:35 +08:00
geekgeekrun
b3c7b287ab fix salary assert method 2026-02-22 13:14:01 +08:00
geekgeekrun
0ffab64653 add button to fill common config field 2026-02-22 10:35:06 +08:00
geekgeekrun
b94520e00a fix salaryMarkAsNotSuitLabelText 2026-02-22 10:11:58 +08:00
geekgeekrun
cac32b934a consume fieldsForUseCommonConfig in real run 2026-02-21 20:58:16 +08:00
geekgeekrun
0eb64a1c69 fix expectCityList in config is not fill back 2026-02-21 20:20:26 +08:00
geekgeekrun
ce651c8f7e move logic of mainWindow?.webContents.send 'common-job-condition-config-updated' 2026-02-21 16:27:34 +08:00
geekgeekrun
013d76726b add listener for common-job-condition-config-updated; enhance common config show in ghosting reminder ui 2026-02-21 10:20:36 +08:00
geekgeekrun-maintainer
4847e8e8d1 更新 README.md 2026-02-21 04:15:04 +08:00
geekgeekrun
2980aebf8d fix when change the checkbox of fieldsForUseCommonConfig, validate error message still visible 2026-02-20 20:42:59 +08:00
geekgeekrun
31236acda4 add common-job-condition-config migrate logic; add commonJobConditionConfig fill back logic 2026-02-20 16:08:41 +08:00
geekgeekrun
8506921ef2 enhance show logic of template dropdown 2026-02-20 12:43:09 +08:00
geekgeekrun
7561412d18 add logic to show common-job-condition-config window in ui 2026-02-20 12:33:12 +08:00
geekgeekrun
a8a6bd9174 enhance show logic of mark as not suit 2026-02-20 11:23:23 +08:00
geekgeekrun
64d10876c3 add checkbox and readonly input box about commonJobConditionConfig 2026-02-20 10:54:06 +08:00
geekgeekrun-maintainer
d0101cfdce 更新 README.md 2026-02-20 08:26:46 +08:00
geekgeekrun
ba7c61f22b add ui of commonJobConditionConfig 2026-02-19 19:16:33 +08:00
geekgeekrun
18b9868ff0 split parts in side nav bar 2026-02-14 20:13:02 +08:00
geekgeekrun
6743649f04 ui-v0.15.2 2026-02-12 08:00:32 +08:00
geekgeekrun
81f990084d Merge branch 'feature/ui' 2026-02-12 07:36:26 +08:00
geekgeekrun
7aa2cc1485 enhance ui text 2026-02-12 07:33:15 +08:00
geekgeekrun
60928b618c enhance log util about fail to JSON.stringify 2026-02-12 07:33:04 +08:00
geekgeekrun
01ce8c996d fix in production build process.env.NODE_ENV is still development 2026-02-12 07:00:30 +08:00
geekgeekrun
b8f6042f36 print log when launch with process.env.NODE_ENV === 'development' 2026-02-12 06:06:41 +08:00
geekgeekrun
1dad234dbc fix some feature cannot use on production build 2026-02-12 05:54:31 +08:00
geekgeekrun
3155017e73 ui-v0.15.1 2026-02-12 00:23:54 +08:00
geekgeekrun
b3dfd827ea Merge branch 'feature/ui' 2026-02-12 00:19:06 +08:00
geekgeekrun
960d937100 fix if blockCompanyNameRegMatchStrategy field show check; 2026-02-12 00:17:07 +08:00
geekgeekrun
69e5516898 ui-v0.15.0 2026-02-11 23:13:44 +08:00
geekgeekrun
333b9a4558 Merge branch 'feature/ui' 2026-02-11 23:12:52 +08:00
geekgeekrun
7e55641d86 Merge branch 'feature/multi-process' into feature/ui 2026-02-11 23:12:12 +08:00
geekgeekrun
eac79a9661 refactor code about collect job info 2026-02-06 05:43:53 +08:00
41 changed files with 3324 additions and 1158 deletions

View File

@@ -1,7 +1,5 @@
# 牛人快跑 - GeekGeekRun
**->维护者正在疯狂求职中<-**
一款可以帮助你在BOSS直聘上**自动批量开聊BOSS**的脚本基于Puppeteer。
与每一位牛人站在一起
@@ -11,8 +9,8 @@
各行各业,无论你是小白还是大佬,都能通过几步简单的配置,快速开始求职!
## TO 有求职之外其他的朋友
本程序的目标,是帮助求职者尽快寻得一个能让他为之**挥洒汗水**、**努力拼搏**的**事业**。如果有其它目的,请勿下载使用。
## TO 有求职之外其他用途的朋友
本程序的目标,是帮助求职者调研求职市场行情或寻找工作。如果有其它用途,请勿下载使用。
## 程序有哪些功能?运行逻辑是什么?怎样使用?
@@ -152,8 +150,8 @@ BOSS不明原因已读不回简历就是投不出去
- 本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘**不会泄露给第三方**也不会进行除自动开聊BOSS以外的行为**请勿向他人泄漏您的Cookie**。
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的BOSS直聘账号**进行求职。
- 本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
- 您所在公司可能会对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;上级/HRBP团队可能会从 IT 团队处获取到监控数据,从而了解团队成员离职倾向。如果您不希望上级/HRBP团队了解到您正在求职,建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及上级/HRBP报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**。
- 您所在公司可能会采购上网行为监控工具或网关(例如奇安信、深信服、绿盟等厂商的产品),对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;您的上级/IT/HR 可能会获取到监控数据,从而了解团队成员离职倾向。如果您不希望您的上级/IT/HR 了解到您正在求职,建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及您的上级/IT/HR 报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**。但由于本程序开源,任何人均可更改本程序源码并重新发布,这一过程中其它开发者是可以加入恶意程序的,因此请从你信任的源下载本程序。
- 本程序**没有内置任何付费功能****下载**、**使用**是**免费**的,任何人可以**免费获得**、**免费使用**。**作者没有利用本程序赚到过任何收入**。如果您是从GitHub以外的地方付费后“购买”的本程序或您被提示“必须付费后才能使用本程序”那**您大概率被骗了**,或者**您下载到了本程序修改版**。**本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方**。
- 本程序**不对您的求职过程与结果负责**为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
- 请在BOSS直聘上自行**屏蔽您不期望投递的公司**如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃请**在BOSS直聘上屏蔽当前公司及与之关联的公司**。

View File

@@ -11,6 +11,9 @@
"staticCombineRecommendJobFilterConditions": [],
"isSkipEmptyConditionForCombineRecommendJobFilter": false,
"expectJobRegExpStr": "",
"expectJobNameRegExpStr": "",
"expectJobTypeRegExpStr": "",
"expectJobDescRegExpStr": "",
"jobNotMatchStrategy": 1,
"jobNotActiveStrategy": 1,
"markAsNotActiveSelectedTimeRange": 7,
@@ -36,5 +39,6 @@
"isSageTimeEnabled": true,
"sageTimeOpTimes": 100,
"sageTimePauseMinute": 15,
"blockCompanyNameRegExpStr": ""
"blockCompanyNameRegExpStr": "",
"fieldsForUseCommonConfig": {}
}

View File

@@ -0,0 +1,12 @@
{
"expectCityList": [],
"expectJobNameRegExpStr": "",
"expectJobTypeRegExpStr": "",
"expectJobDescRegExpStr": "",
"expectCompanies": [],
"blockCompanyNameRegExpStr": "",
"jobDetailRegExpMatchLogic": 1,
"expectSalaryCalculateWay": 1,
"expectSalaryLow": null,
"expectSalaryHigh": null
}

View File

@@ -39,6 +39,7 @@ import {
import { parseSalary } from "@geekgeekrun/sqlite-plugin/dist/utils/parser"
import { waitForSageTimeOrJustContinue } from './sage-time.mjs'
import cityGroupData from './cityGroup.mjs'
import { hasIntersection } from '@geekgeekrun/utils/number.mjs';
const flattedCityList = []
;(cityGroupData?.zpData?.cityGroup ?? []).forEach(it => {
const firstChar = it.firstChar
@@ -112,7 +113,15 @@ export async function initPuppeteer () {
}
}
const targetCompanyList = readConfigFile('target-company-list.json').filter(it => !!it.trim());
const commonJobConditionConfig = readConfigFile('common-job-condition-config.json')
const fieldsForUseCommonConfig = readConfigFile('boss.json').fieldsForUseCommonConfig ?? {}
const targetCompanyList = (
!fieldsForUseCommonConfig.expectCompanies ?
readConfigFile('target-company-list.json')
:
commonJobConditionConfig.expectCompanies
).filter(it => !!it.trim());
const combineRecommendJobFilterType = readConfigFile('boss.json').combineRecommendJobFilterType ?? CombineRecommendJobFilterType.ANY_COMBINE
const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter
@@ -125,14 +134,34 @@ const expectJobRegExpStr = readConfigFile('boss.json').expectJobRegExpStr
const jobNotMatchStrategy = readConfigFile('boss.json').jobNotMatchStrategy ?? MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
const expectCityNotMatchStrategy = readConfigFile('boss.json').expectCityNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
const expectCityList = readConfigFile('boss.json').expectCityList ?? []
const expectCityList = (
!fieldsForUseCommonConfig.city ?
readConfigFile('boss.json').expectCityList
:
commonJobConditionConfig.expectCityList
) ?? []
const strategyScopeOptionWhenMarkJobCityNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkJobCityNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
// salary
const expectSalaryLow = parseFloat(readConfigFile('boss.json').expectSalaryLow) || null
const expectSalaryHigh = parseFloat(readConfigFile('boss.json').expectSalaryHigh) || null
const expectSalaryCalculateWay = readConfigFile('boss.json').expectSalaryCalculateWay ?? SalaryCalculateWay.MONTH_SALARY
const expectSalaryLow = parseFloat(
!fieldsForUseCommonConfig.salary ?
readConfigFile('boss.json').expectSalaryLow
:
commonJobConditionConfig.expectSalaryLow
) || null
const expectSalaryHigh = parseFloat(
!fieldsForUseCommonConfig.salary ?
readConfigFile('boss.json').expectSalaryHigh
:
commonJobConditionConfig.expectSalaryHigh
) || null
const expectSalaryCalculateWay = (
!fieldsForUseCommonConfig.salary ?
readConfigFile('boss.json').expectSalaryCalculateWay
:
commonJobConditionConfig.expectSalaryCalculateWay
) ?? SalaryCalculateWay.MONTH_SALARY
const expectSalaryNotMatchStrategy = readConfigFile('boss.json').expectSalaryNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
const isSalaryFilterEnabled = expectSalaryLow || expectSalaryHigh
const strategyScopeOptionWhenMarkSalaryNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkSalaryNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
@@ -153,7 +182,12 @@ expectWorkExpList = Array.from(expectWorkExpListSet)
const expectWorkExpNotMatchStrategy = readConfigFile('boss.json').expectWorkExpNotMatchStrategy ?? MarkAsNotSuitOp.NO_OP
const strategyScopeOptionWhenMarkJobWorkExpNotMatch = readConfigFile('boss.json').strategyScopeOptionWhenMarkJobWorkExpNotMatch ?? StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
let jobDetailRegExpMatchLogic = readConfigFile('boss.json').jobDetailRegExpMatchLogic ?? JobDetailRegExpMatchLogic.EVERY
let jobDetailRegExpMatchLogic = (
!fieldsForUseCommonConfig.jobDetail ?
readConfigFile('boss.json').jobDetailRegExpMatchLogic
:
commonJobConditionConfig.jobDetailRegExpMatchLogic
) ?? JobDetailRegExpMatchLogic.EVERY
const markAsNotActiveSelectedTimeRange = (() => {
let n = readConfigFile('boss.json').markAsNotActiveSelectedTimeRange
@@ -176,8 +210,9 @@ let {
expectJobNameRegExpStr,
expectJobTypeRegExpStr,
expectJobDescRegExpStr,
} = readConfigFile('boss.json')
} = !fieldsForUseCommonConfig.jobDetail ? readConfigFile('boss.json') : commonJobConditionConfig
if (
!fieldsForUseCommonConfig.jobDetail &&
expectJobRegExpStr &&
!expectJobNameRegExpStr &&
!expectJobTypeRegExpStr &&
@@ -247,7 +282,12 @@ const recommendJobPageUrl = `https://www.zhipin.com/web/geek/jobs`
const expectCompanySet = new Set(targetCompanyList)
const enableCompanyAllowList = Boolean(expectCompanySet.size)
const blockCompanyNameRegExpStr = readConfigFile('boss.json').blockCompanyNameRegExpStr ?? ''
const blockCompanyNameRegExpStr = (
!fieldsForUseCommonConfig.blockCompanyNameRegExpStr ?
readConfigFile('boss.json').blockCompanyNameRegExpStr
:
commonJobConditionConfig.blockCompanyNameRegExpStr
) ?? ''
const blockCompanyNameRegExp = (() => {
if (!blockCompanyNameRegExpStr?.trim()) {
return null
@@ -947,20 +987,24 @@ async function toRecommendPage (hooks) {
function checkIfSalarySuit(salaryDesc) {
const salaryData = parseSalary(salaryDesc)
if (expectSalaryCalculateWay === SalaryCalculateWay.MONTH_SALARY) {
if (expectSalaryHigh && salaryData.high > expectSalaryHigh) {
return false
let ourSalaryInterval = [expectSalaryLow ?? null, expectSalaryHigh ?? null]
if (ourSalaryInterval.every(it => !isNaN(parseFloat(it)))) {
ourSalaryInterval = ourSalaryInterval.sort((a, b) => a - b)
}
if (expectSalaryLow && salaryData.high < expectSalaryLow) {
return false
}
} else if (expectSalaryCalculateWay === SalaryCalculateWay.ANNUAL_PACKAGE) {
const theirSalaryInterval = [salaryData.low ?? null, salaryData.high ?? null]
return hasIntersection(theirSalaryInterval, ourSalaryInterval)
}
else if (expectSalaryCalculateWay === SalaryCalculateWay.ANNUAL_PACKAGE) {
const salaryDataMonth = salaryData.month || 12
if (expectSalaryHigh && (salaryData.high * salaryDataMonth) / 10 > expectSalaryHigh) {
return false
}
if (expectSalaryLow && (salaryData.high * salaryDataMonth) / 10 < expectSalaryLow) {
return false
let ourSalaryInterval = [expectSalaryLow ?? null, expectSalaryHigh ?? null]
if (ourSalaryInterval.every(it => !isNaN(parseFloat(it)))) {
ourSalaryInterval = ourSalaryInterval.sort((a, b) => a - b)
}
const theirSalaryInterval = [salaryData.low ?? null, salaryData.high ?? null].map(
it =>
it === null ? null : (it * salaryDataMonth / 10)
)
return hasIntersection(theirSalaryInterval, ourSalaryInterval)
}
return true
}

View File

@@ -11,16 +11,114 @@ 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', 'llm.json']
import defaultCommonJobConditionConfig from './default-config-file/common-job-condition-config.json' assert { type: 'json' }
export const configFileNameList = ['boss.json', 'dingtalk.json', 'target-company-list.json', 'llm.json', 'common-job-condition-config.json']
const defaultConfigFileContentMap = {
'boss.json': JSON.stringify(defaultBossConf),
'dingtalk.json': JSON.stringify(defaultDingtalkConf),
'target-company-list.json': JSON.stringify(defaultTargetCompanyListConf),
'llm.json': JSON.stringify(defaultLlmConf)
'llm.json': JSON.stringify(defaultLlmConf),
'common-job-condition-config.json': JSON.stringify(defaultCommonJobConditionConfig)
}
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
export const configFolderPath = path.join(
runtimeFolderPath,
'config'
)
export const writeConfigFile = async (fileName, content, { isSync } = {}) => {
const filePath = path.join(configFolderPath, fileName)
const fileContent = JSON.stringify(content)
if (isSync) {
fs.writeFileSync(
filePath,
fileContent
)
}
else {
return fsPromise.writeFile(
filePath,
fileContent
)
}
}
if (
!fs.existsSync(
path.join(configFolderPath, 'common-job-condition-config.json')
)
) {
let bossConfig = null
if (
fs.existsSync(
path.join(configFolderPath, 'boss.json')
)
) {
fs.existsSync(
path.join(configFolderPath, 'boss.json')
)
try {
bossConfig = JSON.parse(
fs.readFileSync(
path.join(configFolderPath, 'boss.json')
)
)
}
catch {}
}
if (bossConfig) {
Object.keys(defaultCommonJobConditionConfig).forEach(
key => {
if (Object.hasOwn(bossConfig, key)) {
defaultCommonJobConditionConfig[key] = bossConfig[key]
}
}
)
let {
expectJobRegExpStr,
expectJobNameRegExpStr,
expectJobTypeRegExpStr,
expectJobDescRegExpStr,
} = bossConfig
if (
expectJobRegExpStr &&
!expectJobNameRegExpStr &&
!expectJobTypeRegExpStr &&
!expectJobDescRegExpStr
) {
expectJobNameRegExpStr = expectJobRegExpStr
expectJobTypeRegExpStr = expectJobRegExpStr
expectJobDescRegExpStr = expectJobRegExpStr
}
Object.assign(defaultCommonJobConditionConfig, {
expectJobNameRegExpStr,
expectJobTypeRegExpStr,
expectJobDescRegExpStr
})
}
let targetCompanyList = null
if (
fs.existsSync(
path.join(configFolderPath, 'target-company-list.json')
)
) {
targetCompanyList = JSON.parse(
fs.readFileSync(
path.join(configFolderPath, 'target-company-list.json')
)
)
}
if (targetCompanyList) {
defaultCommonJobConditionConfig.expectCompanies = targetCompanyList ?? []
}
writeConfigFile('common-job-condition-config.json', defaultCommonJobConditionConfig, { isSync: true })
if (bossConfig) {
if (!bossConfig.fieldsForUseCommonConfig) {
bossConfig.fieldsForUseCommonConfig = {}
}
writeConfigFile('boss.json', bossConfig, { isSync: true })
}
}
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
const ensureRuntimeFolderPathExist = () => {
if (!fs.existsSync(runtimeFolderPath)) {
fs.mkdirSync(runtimeFolderPath)
@@ -35,11 +133,6 @@ const ensureRuntimeFolderPathExist = () => {
}
})
}
export const configFolderPath = path.join(
runtimeFolderPath,
'config'
)
export const ensureConfigFileExist = () => {
ensureRuntimeFolderPathExist()
;configFileNameList.forEach(
@@ -82,15 +175,6 @@ export const readConfigFile = (fileName) => {
return o
}
export const writeConfigFile = async (fileName, content) => {
const filePath = path.join(configFolderPath, fileName)
const fileContent = JSON.stringify(content)
return fsPromise.writeFile(
filePath,
fileContent
)
}
export const storageFilePath = path.join(
runtimeFolderPath,
'storage'

View File

@@ -7,6 +7,56 @@ import transformerDirective from '@unocss/transformer-directives'
import Replace from 'unplugin-replace/vite'
process.env = { ...process.env, ...loadEnv(process.env.NODE_ENV!, process.cwd()) }
const mainPlugins = [
externalizeDepsPlugin({
exclude: [
'@geekgeekrun/utils',
'find-chrome-bin',
'@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
]
}),
Replace({
delimiters: ['', ''],
sourcemap: true,
include: ['**/src/main/utils/gtag/Analytics.ts'],
values: [
{
find: /<measurement_id>/g,
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string
},
{
find: /<api_secret>/g,
replacement: process.env.VITE_APP_GTAG_API_SECRET as string
}
]
})
]
const preloadPlugins = [externalizeDepsPlugin()]
const rendererPlugins = [
vue(),
UnoCSS({
presets: [presetUno(), presetAttributify(), presetIcons()],
transformers: [transformerDirective()]
})
]
if (process.env.NODE_ENV) {
;[mainPlugins, preloadPlugins, rendererPlugins].forEach((pluginList) => {
pluginList.push(
Replace({
delimiters: ['', ''],
sourcemap: true,
include: ['**'],
values: [
{
find: /process.env.NODE_ENV/g,
replacement: `'${process.env.NODE_ENV}'` as string
}
]
})
)
})
}
export default defineConfig({
main: {
build: {
@@ -15,33 +65,10 @@ export default defineConfig({
},
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
},
plugins: [
externalizeDepsPlugin({
exclude: [
'@geekgeekrun/utils',
'find-chrome-bin',
'@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
]
}),
Replace({
delimiters: ['', ''],
sourcemap: true,
include: ['**/src/main/utils/gtag/Analytics.ts'],
values: [
{
find: /<measurement_id>/g,
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string
},
{
find: /<api_secret>/g,
replacement: process.env.VITE_APP_GTAG_API_SECRET as string
}
]
})
]
plugins: mainPlugins
},
preload: {
plugins: [externalizeDepsPlugin()],
plugins: preloadPlugins,
build: {
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
}
@@ -52,13 +79,7 @@ export default defineConfig({
'@renderer': resolve('src/renderer/src')
}
},
plugins: [
vue(),
UnoCSS({
presets: [presetUno(), presetAttributify(), presetIcons()],
transformers: [transformerDirective()]
})
],
plugins: rendererPlugins,
build: {
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
}

View File

@@ -1,6 +1,6 @@
{
"name": "geekgeekrun-ui",
"version": "0.14.2",
"version": "0.17.1",
"description": "BOSS 炸弹 - 自动开聊BOSS助力每位打工人求职",
"main": "./out/main/index.js",
"author": "geekgeekrun",
@@ -87,4 +87,4 @@
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.27"
}
}
}

View File

@@ -1,7 +1,7 @@
{
"version": "0.14.2",
"buildVersion": 32,
"buildTime": 1770299394728,
"buildHash": "1d5fdb9d54c569cb53035dfef0f6157c899ccb06",
"version": "0.17.1",
"buildVersion": 38,
"buildTime": 1772159430810,
"buildHash": "e6489db6c4f596264f6eea972c136367aa556b7a",
"name": "geekgeekrun-ui"
}

View File

@@ -1,2 +1,7 @@
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
export const DEFAULT_CONSTANT_OPEN_CONTENT_SEGS = [
`您好,我对贵公司岗位很感兴趣,希望能有机会进一步沟通,期待您的回复`,
`接下来的聊天,我将根据我简历里的内容,向您进行自我介绍`
]

View File

@@ -18,6 +18,16 @@ export enum RECHAT_LLM_FALLBACK {
EXIT_REMINDER_PROGRAM = 2
}
export enum OPEN_CONTENT_SOURCE {
CONSTANT_CONTENT = 1,
GEMINI_WITH_CHAT_CONTEXT = 2
}
// export enum OPEN_LLM_FALLBACK {
// SEND_CONSTANT_CONTENT = 1,
// EXIT_REMINDER_PROGRAM = 2
// }
export enum RUNNING_STATUS_ENUM {
RUNNING = 0,
NORMAL_EXITED = 1,

View File

@@ -0,0 +1,28 @@
import { ipcMain } from 'electron'
import { createCommonJobConditionConfigWindow } from '../window/commonJobConditionConfigWindow'
import { mainWindow } from '../window/mainWindow'
let commonJobConditionConfigWindow = null
export async function waitForCommonJobConditionDone() {
return new Promise((resolve, reject) => {
commonJobConditionConfigWindow = createCommonJobConditionConfigWindow({
parent: mainWindow!,
modal: true,
show: true
})
let processDone = false
function handler() {
processDone = true
commonJobConditionConfigWindow.close()
}
ipcMain.once('common-job-condition-config-done', handler)
commonJobConditionConfigWindow.on('closed', async () => {
ipcMain.off('common-job-condition-config-done', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED'))
}
})
})
}

View File

@@ -49,64 +49,62 @@ const attachRequestsListener = async (target: Target) => {
return
}
function handleJobDetailPage({ encryptJobId } = { encryptJobId: null }) {
Promise.resolve()
.then(async () => {
if (!encryptJobId) {
return
}
try {
await page.waitForFunction(
({ encryptJobId }) => {
return (
location.href.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
document.readyState === 'complete'
)
},
undefined,
{ encryptJobId }
// FIXME: might not work
async function handleJobDetailPage({ encryptJobId } = { encryptJobId: null }) {
if (!encryptJobId) {
return
}
try {
await page.waitForFunction(
({ encryptJobId }) => {
return (
location.href.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
(!!document.querySelector('#main .job-banner') ||
!!document.documentElement.innerText?.includes(`您访问的页面不存在`))
)
const htmlContent = await page.content()
if (htmlContent) {
const $ = cheerio.load(htmlContent)
const [jobBannerEl] = $('#main .job-banner') ?? []
if (!jobBannerEl) {
console.log(`access might be blocked`)
if (
htmlContent.includes(`您访问的页面不存在`) ||
location.href === `https://www.zhipin.com/`
) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.DELETED,
lastSeenDate: new Date()
})
}
},
undefined,
{ encryptJobId }
)
const htmlContent = await page.content()
if (htmlContent) {
const $ = cheerio.load(htmlContent)
const [jobBannerEl] = $('#main .job-banner') ?? []
if (!jobBannerEl) {
console.log(`access might be blocked`)
if (
htmlContent.includes(`您访问的页面不存在`) ||
location.href === `https://www.zhipin.com/`
) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.DELETED,
lastSeenDate: new Date()
})
}
} else {
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
if (jobStatusTextEl) {
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
if ([`职位已关闭`].includes(jobStatusText)) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.CLOSED,
lastSeenDate: new Date()
})
} else {
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
if (jobStatusTextEl) {
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
if ([`职位已关闭`].includes(jobStatusText)) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.CLOSED,
lastSeenDate: new Date()
})
} else {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
}
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
}
} catch {
//
}
})
.catch(() => void 0)
}
} catch {
//
}
}
if (page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
@@ -200,7 +198,7 @@ const attachRequestsListener = async (target: Target) => {
)
const requestBody = new URLSearchParams(response.request().postData())
const securityIdInRequest = requestBody.get("securityId")
const securityIdInRequest = requestBody.get('securityId')
const currentJobSecurityId = currentJobData?.securityId
if (securityIdInRequest !== currentJobSecurityId) {

View File

@@ -2,8 +2,6 @@ import { ipcMain, shell, app, dialog, BrowserWindow } from 'electron'
import path from 'path'
import * as childProcess from 'node:child_process'
import {
ensureConfigFileExist,
configFileNameList,
readConfigFile,
writeConfigFile,
readStorageFile,
@@ -34,7 +32,7 @@ import {
requestNewMessageContent
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
autoReminderPromptTemplateFileName,
defaultPromptMap,
writeDefaultAutoRemindPrompt
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
@@ -58,23 +56,10 @@ import {
waitForUserApproveAgreement
} from '../../../features/first-launch-notice-window'
import { getLastUsedAndAvailableBrowser } from '../../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { waitForCommonJobConditionDone } from '../../../features/common-job-condition'
import { ensureConfigFileExist } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
export default function initIpc() {
ipcMain.handle('fetch-config-file-content', async () => {
const configFileContentList = configFileNameList.map((fileName) => {
return readConfigFile(fileName)
})
const result = {
config: {}
}
configFileNameList.forEach((fileName, index) => {
result.config[fileName] = configFileContentList[index]
})
return result
})
ipcMain.handle('save-config-file-from-ui', async (ev, payload) => {
payload = JSON.parse(payload)
ensureConfigFileExist()
@@ -187,6 +172,9 @@ export default function initIpc() {
if (hasOwn(payload, 'blockCompanyNameRegMatchStrategy')) {
bossConfig.blockCompanyNameRegMatchStrategy = payload.blockCompanyNameRegMatchStrategy
}
if (hasOwn(payload, 'fieldsForUseCommonConfig')) {
bossConfig.fieldsForUseCommonConfig = payload.fieldsForUseCommonConfig
}
promiseArr.push(writeConfigFile('boss.json', bossConfig))
@@ -373,7 +361,9 @@ export default function initIpc() {
}
subProcessOfOpenBossSite = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=launchBossSite`],
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=launchBossSite`]
: [`--mode=launchBossSite`],
{
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe']
@@ -476,17 +466,19 @@ export default function initIpc() {
const res = (await readConfigFile('resumes.json'))?.[0]
return res?.content ?? null
})
ipcMain.on('no-reply-reminder-prompt-edit', async () => {
const template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false })
ipcMain.on('no-reply-reminder-prompt-edit', async (_, { type }) => {
const template = await readStorageFile(defaultPromptMap[type].fileName, {
isJson: false
})
if (!template) {
await writeDefaultAutoRemindPrompt()
await writeDefaultAutoRemindPrompt({ type })
}
const filePath = path.join(storageFilePath, autoReminderPromptTemplateFileName)
const filePath = path.join(storageFilePath, defaultPromptMap[type].fileName)
shell.openPath(filePath)
})
ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close())
ipcMain.handle('check-if-auto-remind-prompt-valid', async () => {
await getValidTemplate()
ipcMain.handle('check-if-auto-remind-prompt-valid', async (_, { type }) => {
await getValidTemplate({ type })
})
ipcMain.handle('check-is-resume-content-valid', async () => {
const res = (await readConfigFile('resumes.json'))?.[0]
@@ -496,8 +488,8 @@ export default function initIpc() {
const res = (await readConfigFile('resumes.json'))?.[0]
return resumeContentEnoughDetect(res)
})
ipcMain.handle('overwrite-auto-remind-prompt-with-default', async () => {
await writeDefaultAutoRemindPrompt()
ipcMain.handle('overwrite-auto-remind-prompt-with-default', async (_, { type }) => {
await writeDefaultAutoRemindPrompt({ type })
})
ipcMain.handle('check-if-llm-config-list-valid', async () => {
const llmConfigList = await readConfigFile('llm.json')
@@ -605,6 +597,12 @@ export default function initIpc() {
}
}
})
ipcMain.handle('common-job-condition-config', async () => {
await waitForCommonJobConditionDone()
mainWindow?.webContents.send('common-job-condition-config-updated', {
config: await readConfigFile('common-job-condition-config.json')
})
})
ipcMain.handle('exit-app-immediately', () => {
app.exit(0)

View File

@@ -7,7 +7,6 @@ import {
import { randomUUID } from 'node:crypto'
import { connectToDaemon } from './connect-to-daemon'
const isUiDev = process.env.NODE_ENV === 'development'
export async function ensureIpcPipeName({ isReset } = {}) {
if (isReset) {
await writeStorageFile('ipc-pipe-name', '', { isJson: false })
@@ -29,7 +28,9 @@ export async function launchDaemon() {
// 添加参数使守护进程在后台运行,不显示 UI
daemonProcess = spawn(
process.argv[0],
isUiDev ? [process.argv[1], `--mode=launchDaemon`] : [`--mode=launchDaemon`],
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=launchDaemon`]
: [`--mode=launchDaemon`],
{
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
detached: true,

View File

@@ -60,7 +60,10 @@ const pickLlmConfigFromList = (llmConfigList, blockModelSet) => {
// let _index = 0
const RESUME_PLACEHOLDER = `__REPLACE_REAL_RESUME_HERE__`
const defaultPrompt = `**核心指令:**
export const defaultPromptMap = {
rechat: {
fileName: 'auto-reminder-resume-system-message-template.md',
content: `**核心指令:**
你是一个智能求职助手需要根据用户简历生成30字左右的提醒消息满足以下要求
1. 每次生成需满足:
- √ 包含1个核心技能 + 1个成果量化
@@ -94,15 +97,21 @@ const defaultPrompt = `**核心指令:**
**输出格式:**
请确保仅回复一句话以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
},
open: {
fileName: 'auto-reminder-open-message-template.md',
content:
'请根据我的简历帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容但需要表达出应聘意向。请确保仅响应一句话以JSON响应数据结构参考`{"response": "这里是将会发送给招聘者的内容"}`'
}
if (!template.includes(RESUME_PLACEHOLDER)) {
}
export const getValidTemplate = async ({ type }) => {
let template = await readStorageFile(defaultPromptMap[type].fileName, { isJson: false })
if (!template) {
await writeDefaultAutoRemindPrompt({ type })
template = defaultPromptMap[type].content
}
if (type === 'rechat' && !template.includes(RESUME_PLACEHOLDER)) {
const e = new Error(`简历内容占位符字符串不存在。占位字符串是 ${RESUME_PLACEHOLDER}`)
e.name = `RESUME_PLACEHOLDER_NOT_EXIST`
throw e
@@ -110,8 +119,19 @@ export const getValidTemplate = async () => {
return template
}
export const writeDefaultAutoRemindPrompt = async () => {
await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false })
export const writeDefaultAutoRemindPrompt = async ({ type }) => {
switch (type) {
case 'rechat':
await writeStorageFile(defaultPromptMap[type].fileName, defaultPromptMap[type].content, {
isJson: false
})
break
case 'open':
await writeStorageFile(defaultPromptMap[type].fileName, defaultPromptMap[type].content, {
isJson: false
})
break
}
}
export const requestNewMessageContent = async (
@@ -119,24 +139,26 @@ export const requestNewMessageContent = async (
{
requestScene,
llmConfigIdForPick
}: { requestScene?: RequestSceneEnum; llmConfigIdForPick?: string[] } = {}
}: {
requestScene?: RequestSceneEnum
llmConfigIdForPick?: string[]
} = {}
) => {
const template = await getValidTemplate()
const systemMessageTemplate = await getValidTemplate({ type: 'rechat' })
const resumeObject = (await readConfigFile('resumes.json'))?.[0]
const resumeContent = formatResumeJsonToMarkdown(resumeObject)
const chatList = [
{
role: 'system',
content: template.replace(RESUME_PLACEHOLDER, resumeContent)
content: systemMessageTemplate.replace(RESUME_PLACEHOLDER, resumeContent)
}
]
const openMessageTemplate = await getValidTemplate({ type: 'open' })
chatList.push({
role: 'user',
content:
'请根据我的简历帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容但需要表达出应聘意向。请确保仅响应一句话以JSON响应数据结构参考`{"response": "这里是将会发送给招聘者的内容"}`'
content: openMessageTemplate
})
// chatRecords = chatRecords.slice(chatRecords.length - _index)
// debugger
for (const record of chatRecords) {
const assistantJsonContent = JSON.stringify({
response: record.text
@@ -246,14 +268,18 @@ export const requestNewMessageContent = async (
}
}
export async function sendGptContent(page: Page, chatRecords) {
export async function getGptContent(chatRecords) {
const textToSend = (
await requestNewMessageContent(chatRecords, {
requestScene: RequestSceneEnum.readNoReplyAutoReminder
})
).responseText
return textToSend
}
export async function sendMessage(page: Page, textToSend: string) {
const chatInputSelector = `.chat-conversation .message-controls .chat-input`
const chatInputHandle = await page.$(chatInputSelector)
const chatInputHandle = (await page.$(chatInputSelector))!
await chatInputHandle.click()
await sleep(500)
await chatInputHandle.click()

View File

@@ -1,7 +1,7 @@
import { bootstrap, launchBoss } from './bootstrap'
import { MsgStatus, type ChatListItem } from './types'
import { Browser, Page } from 'puppeteer'
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
import { getGptContent, sendLookForwardReplyEmotion, sendMessage } from './boss-operation'
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
import { app, dialog } from 'electron'
@@ -24,6 +24,7 @@ import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
import { messageForSaveFilter } from '../../../common/utils/chat-list'
import {
AUTO_CHAT_ERROR_EXIT_CODE,
OPEN_CONTENT_SOURCE,
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK
} from '../../../common/enums/auto-start-chat'
@@ -40,6 +41,7 @@ import { loginWithCookieAssistant } from '../../features/login-with-cookie-assis
import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../common/constant'
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在退出')
@@ -58,12 +60,21 @@ const rechatLlmFallback =
readConfigFile('boss.json').autoReminder?.rechatLlmFallback ??
RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
const expectJobTypeRegExpStr = readConfigFile('boss.json').expectJobTypeRegExpStr
const fieldsForUseCommonConfig = readConfigFile('boss.json').fieldsForUseCommonConfig ?? {}
const commonJobConditionConfig = readConfigFile('common-job-condition-config.json') ?? {}
const expectJobTypeRegExpStr =
(!fieldsForUseCommonConfig.jobDetail ? readConfigFile('boss.json') : commonJobConditionConfig)
?.expectJobTypeRegExpStr ?? ''
const onlyRemindBossWithExpectJobType =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithExpectJobType ??
!!expectJobTypeRegExpStr
const blockCompanyNameRegExpStr = readConfigFile('boss.json').blockCompanyNameRegExpStr ?? ''
const blockCompanyNameRegExpStr =
(!fieldsForUseCommonConfig.blockCompanyNameRegExpStr
? readConfigFile('boss.json')
: commonJobConditionConfig
)?.blockCompanyNameRegExpStr ?? ''
const blockCompanyNameRegExp = (() => {
if (!blockCompanyNameRegExpStr?.trim()) {
return null
@@ -78,6 +89,21 @@ const onlyRemindBossWithoutBlockCompanyName =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithoutBlockCompanyName ??
!!blockCompanyNameRegExp
const openContentSource = readConfigFile('boss.json').autoReminder?.openContentSource ?? OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
const constantOpenContent = (() => {
let constantOpenContent = readConfigFile('boss.json').autoReminder?.constantOpenContent ?? ''
if (constantOpenContent?.trim?.()) {
return constantOpenContent
} else {
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
constantOpenContent = DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join(``)
} else {
constantOpenContent = DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
}
}
return constantOpenContent
})()
const dbInitPromise = initDb(getPublicDbFilePath())
export const pageMapByName: {
@@ -573,30 +599,50 @@ const mainLoop = async () => {
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
) {
await sleepWithRandomDelay(3250)
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)
const messageList = historyMessageList
.filter((it) => it.bizType !== 101 && it.isSelf)
.slice(-recentMessageQuantityForLlm)
if (!messageList?.length) {
if (openContentSource === OPEN_CONTENT_SOURCE.CONSTANT_CONTENT) {
await sendMessage(pageMapByName.boss!, constantOpenContent)
gtag('rnrr_llm_content_sent')
} catch (err) {
console.log(err)
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) {
await sendLookForwardReplyEmotion(pageMapByName.boss!)
} else {
try {
const textToSend = await getGptContent(messageList)
await sendMessage(pageMapByName.boss!, textToSend)
gtag('rnrr_llm_content_sent')
} catch (err) {
console.log(err)
await sendMessage(pageMapByName.boss!, constantOpenContent)
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')
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
try {
const textToSend = await getGptContent(messageList)
await sendMessage(pageMapByName.boss!, textToSend)
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

View File

@@ -1,8 +1,16 @@
import overrideConsole from './utils/overrideConsole'
import minimist from 'minimist'
import { runCommon } from './features/run-common'
import { launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'
import { app } from 'electron'
const isUiDev = process.env.NODE_ENV === 'development'
const enableLogToFile = process.env.GEEKGEEKRUN_ENABLE_LOG_TO_FILE === String(1)
if (isUiDev || enableLogToFile) {
overrideConsole()
}
console.log('NODE_ENV:', process.env.NODE_ENV)
// 捕获未处理的 EPIPE 错误
process.on('uncaughtException', (err) => {
if (err?.code === 'EPIPE' || err?.code === 'ERR_STREAM_DESTROYED') {
@@ -11,9 +19,9 @@ process.on('uncaughtException', (err) => {
throw err
})
const isUiDev = process.env.NODE_ENV === 'development'
console.log('argv:', process.argv)
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
console.log(commandlineArgs)
console.log('parsed commandline args:', commandlineArgs)
const runMode = commandlineArgs['mode']

View File

@@ -5,8 +5,10 @@ import os from 'node:os'
import fs from 'node:fs'
import {
ensureStorageFileExist,
readStorageFile,
writeStorageFile
writeStorageFile,
configFileNameList,
readConfigFile,
readStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
export default function initPublicIpc() {
@@ -110,4 +112,19 @@ export default function initPublicIpc() {
}
return null
})
ipcMain.handle('fetch-config-file-content', async () => {
const configFileContentList = configFileNameList.map((fileName) => {
return readConfigFile(fileName)
})
const result = {
config: {}
}
configFileNameList.forEach((fileName, index) => {
result.config[fileName] = configFileContentList[index]
})
return result
})
}

View File

@@ -0,0 +1,75 @@
import path from 'node:path'
import os from 'node:os'
import fs from 'node:fs'
import dayjs from 'dayjs'
export default function overrideConsole() {
const originConsoleLog = console.log.bind(console)
const originConsoleWarn = console.warn.bind(console)
const originConsoleError = console.error.bind(console)
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
const logDirPath = path.join(runtimeFolderPath, 'log')
if (!fs.existsSync(logDirPath)) {
fs.mkdirSync(logDirPath, { recursive: true })
}
const logFileStream = fs.createWriteStream(path.join(logDirPath, `log.log`), {
flags: 'a' // 追加模式
})
const warnFileStream = fs.createWriteStream(path.join(logDirPath, `warn.log`), {
flags: 'a' // 追加模式
})
const errorFileStream = fs.createWriteStream(path.join(logDirPath, `error.log`), {
flags: 'a' // 追加模式
})
console.log = (...args: any[]) => {
const lineHead = `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} [log][PID=${process.pid}]`
originConsoleLog(lineHead, ...args)
logFileStream.write(
[
lineHead,
args.map((arg) => {
try {
return JSON.stringify(arg)
} catch (err) {
return `[[${JSON.stringify(err?.toString())}]]`
}
})
].join(' ') + '\n'
)
}
console.warn = (...args: any[]) => {
const lineHead = `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} [warn][PID=${process.pid}]`
originConsoleWarn(lineHead, ...args)
warnFileStream.write(
[
lineHead,
args.map((arg) => {
try {
return JSON.stringify(arg)
} catch (err) {
return `[[${JSON.stringify(err?.toString())}]]`
}
})
].join(' ') + '\n'
)
}
console.error = (...args: any[]) => {
const lineHead = `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} [warn][PID=${process.pid}]`
originConsoleError(lineHead, ...args)
errorFileStream.write(
[
lineHead,
args.map((arg) => {
try {
return JSON.stringify(arg)
} catch (err) {
return `[[${JSON.stringify(err?.toString())}]]`
}
})
].join(' ') + '\n'
)
}
}

View File

@@ -31,6 +31,7 @@ export function createBrowserAssistantWindow(
minWidth: 800,
height: 400,
resizable: true,
frame: true,
show: false,
autoHideMenuBar: true,
webPreferences: {

View File

@@ -62,7 +62,9 @@ export function createBrowserDownloadProgressWindow(
}
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=downloadDependenciesForInit`],
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=downloadDependenciesForInit`]
: [`--mode=downloadDependenciesForInit`],
{
stdio: [null, null, null, 'pipe', 'ipc']
}

View File

@@ -0,0 +1,56 @@
import { BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import { writeConfigFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
export let commonJobConditionConfigWindow: BrowserWindow | null = null
export function createCommonJobConditionConfigWindow(
opt?: Electron.BrowserWindowConstructorOptions
): BrowserWindow {
// Create the browser window.
if (commonJobConditionConfigWindow) {
commonJobConditionConfigWindow!.show()
}
commonJobConditionConfigWindow = new BrowserWindow({
width: 1024,
height: 768,
resizable: false,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
commonJobConditionConfigWindow.on('ready-to-show', () => {
commonJobConditionConfigWindow!.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']) {
commonJobConditionConfigWindow.loadURL(
process.env['ELECTRON_RENDERER_URL'] + '#/commonJobConditionConfig'
)
} else {
commonJobConditionConfigWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + '#/commonJobConditionConfig'
)
}
commonJobConditionConfigWindow!.once('closed', () => {
commonJobConditionConfigWindow = null
})
ipcMain.handle('save-common-job-condition-config', async (_ev, payload) => {
await writeConfigFile('common-job-condition-config.json', payload)
commonJobConditionConfigWindow!.close()
})
commonJobConditionConfigWindow!.once('closed', () => {
ipcMain.removeHandler('save-common-job-condition-config')
})
return commonJobConditionConfigWindow!
}

View File

@@ -20,6 +20,7 @@ export function createCookieAssistantWindow(
resizable: true,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
@@ -83,7 +84,9 @@ export function createCookieAssistantWindow(
}
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=launchBossZhipinLoginPageWithPreloadExtension`],
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=launchBossZhipinLoginPageWithPreloadExtension`]
: [`--mode=launchBossZhipinLoginPageWithPreloadExtension`],
{
env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc']

View File

@@ -8,11 +8,12 @@ export function createFirstLaunchNoticeWindow(
// Create the browser window.
firstLaunchNoticeWindow = new BrowserWindow({
width: 960,
height: 640,
resizable: false,
maxWidth: 960,
minHeight: 320,
resizable: true,
show: false,
autoHideMenuBar: true,
frame: false,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -15,6 +15,7 @@ export function createLlmConfigWindow(
resizable: false,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -12,6 +12,7 @@ export function createMainWindow(): BrowserWindow {
minWidth: 1280,
show: false,
autoHideMenuBar: true,
frame: true,
...(process.platform === 'linux'
? {
/* icon */

View File

@@ -17,6 +17,7 @@ export function createReadNoReplyReminderLlmMockWindow(
resizable: false,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -15,6 +15,7 @@ export function createResumeEditorWindow(
resizable: true,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -0,0 +1,733 @@
<template>
<div class="common-job-condition-config" flex flex-col h-full>
<div flex-1 of-auto>
<el-form
ref="formRef"
:model="formContent"
:rules="formRules"
inline-message
w-800px
pt-30px
pb-30px
ml-auto
mr-auto
>
<div mb20px>公共职位筛选条件</div>
<el-form-item prop="expectCompanies" mb0>
<div
font-size-14px
flex
mb6px
:style="{
justifyContent: 'space-between',
alignItems: 'baseline',
width: '100%',
lineHeight: '1.25em'
}"
>
<div>
期望投递公司
<br /><span font-size-12px
><b color-orange>逗号分隔</b>不区分大小写输入框留空表示不筛选</span
>
</div>
<el-dropdown @command="handleExpectCompanyTemplateClicked">
<el-button size="small"
>公司列表模板 <el-icon class="el-icon--right"><arrow-down /></el-icon
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in expectCompanyTemplateList"
:key="item.name"
:command="item"
>{{ item.name }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-input
v-model="formContent.expectCompanies"
:autosize="{ minRows: 4 }"
max-h-8lh
type="textarea"
placeholder="置空表示“不限公司,任意公司都可以投递”"
@blur="
formContent.expectCompanies = normalizeCommaSplittedStr(formContent.expectCompanies)
"
/>
</el-form-item>
<div class="h-1px bg-#f0f0f0" mt16px mb8px />
<div
ref="blockCompanyNameRegExpSectionEl"
font-size-14px
flex
:style="{
justifyContent: 'space-between',
alignItems: 'baseline',
width: '100%',
lineHeight: '1.25em'
}"
>
<div mb6px>
不期望投递公司<b color-orange>正则</b><br /><span font-size-12px
><b color-orange>正则表达式</b>不区分大小写输入框留空表示不筛选<span
color-orange
>优先级高于上方期望投递公司</span
><br /><b color-red>小心验证</b
>你编写的正则填写太过于宽泛的正则例如`.*`将导致任何职位都不会开聊</span
>
</div>
<el-dropdown @command="handleBlockCompanyNameRegExpTemplateClicked">
<el-button size="small"
>公司列表模板 <el-icon class="el-icon--right"><arrow-down /></el-icon
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in blockCompanyNameRegExpTemplateList"
:key="item.name"
:command="item"
>{{ item.name }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div
class="block-company-filter-wrap"
:style="{
display: 'flex',
justifyContent: 'space-between',
gap: '10px'
}"
>
<el-form-item prop="blockCompanyNameRegExpStr" mb0 w-full>
<el-input
v-model="formContent.blockCompanyNameRegExpStr"
:autosize="{ minRows: 4 }"
max-h-8lh
type="textarea"
placeholder="置空表示“不限公司,任意公司都不会被标记为不合适”"
@blur="
formContent.blockCompanyNameRegExpStr =
formContent.blockCompanyNameRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
</div>
<div class="h-1px bg-#f0f0f0" mt16px mb16px />
<div mt16px>
<div font-size-14px mb8px>工作地</div>
<div
:style="{
display: 'flex',
justifyContent: 'space-between',
gap: '10px'
}"
>
<el-form-item prop="expectCityList" mb0>
<div
font-size-12px
:style="{
justifyContent: 'space-between',
alignItems: 'center',
width: '100%'
}"
>
<city-chooser v-model="formContent.expectCityList">
<template #default="{ modelValue, showDialog, clearValue }">
<div v-if="modelValue?.length">
<div>当前已选择城市</div>
<div flex flex-wrap gap-10px>
<el-tag v-for="it in modelValue" :key="it">
{{ it }}
</el-tag>
</div>
</div>
<div v-else>
<div>当前未选择任何期望城市将不会按照城市进行筛选</div>
</div>
<div
line-height-1
:style="{
marginTop: modelValue?.length ? '10px' : ''
}"
>
<el-button
size="small"
type="primary"
@click="
() => {
// isDialogVisible = true
showDialog()
gtagRenderer('choose_city_entry_button_clicked')
}
"
>选择城市</el-button
>
<el-button
v-if="modelValue?.length"
size="small"
type="danger"
@click="clearValue"
>清空已选择的所有城市</el-button
>
</div>
</template>
</city-chooser>
</div>
</el-form-item>
</div>
</div>
<div class="h-1px bg-#f0f0f0" mt16px mb16px />
<div mt16px>
<div font-size-14px mb8px>
薪资仅支持按月计算薪资的职位非按月计算薪资职位例如兼职职位实习职位将直接跳过
</div>
<div
:style="{
display: 'flex',
justifyContent: 'space-between',
gap: '10px'
}"
>
<div>
<el-form-item prop="expectSalaryLow" mb10px>
<div w-full>
<div font-size-12px>薪资筛选方式</div>
<el-select
v-model="formContent.expectSalaryCalculateWay"
@change="handleExpectSalaryCalculateWayChanged"
>
<el-option
v-for="op in expectSalaryCalculateWayOption"
:key="op.value"
:label="op.name"
:value="op.value"
>{{ op.name }}</el-option
>
</el-select>
</div>
</el-form-item>
<el-form-item prop="expectSalaryLow" mb10px>
<div>
<div font-size-12px>期望薪资范围</div>
<div>
<el-input-number
v-model="formContent.expectSalaryLow"
controls-position="right"
:min="0"
:step="0.25"
placeholder="不设置"
@change="
() => {
gtagRenderer('expect_salary_low_changed')
ensureSalaryRangeCorrect({ formContent })
}
"
>
<template #prefix>下限</template>
<template #suffix>
<template v-if="formContent.expectSalaryLow">
<template
v-if="
formContent.expectSalaryCalculateWay ===
SalaryCalculateWay.MONTH_SALARY
"
>k</template
>
<template
v-if="
formContent.expectSalaryCalculateWay ===
SalaryCalculateWay.ANNUAL_PACKAGE
"
>W</template
>
</template>
</template>
</el-input-number>
-
<el-input-number
v-model="formContent.expectSalaryHigh"
controls-position="right"
:min="0"
:step="0.25"
placeholder="不设置"
@change="
() => {
gtagRenderer('expect_salary_high_changed')
ensureSalaryRangeCorrect({ formContent })
}
"
>
<template #prefix>上限</template>
<template #suffix>
<template v-if="formContent.expectSalaryHigh">
<template
v-if="
formContent.expectSalaryCalculateWay ===
SalaryCalculateWay.MONTH_SALARY
"
>k</template
>
<template
v-if="
formContent.expectSalaryCalculateWay ===
SalaryCalculateWay.ANNUAL_PACKAGE
"
>W</template
>
</template>
</template>
</el-input-number>
</div>
</div>
</el-form-item>
<el-form-item
v-if="
formContent.expectSalaryCalculateWay === SalaryCalculateWay.ANNUAL_PACKAGE &&
(formContent.expectSalaryLow || formContent.expectSalaryHigh)
"
mb10px
>
<div>
<div font-size-12px>薪资范围满足以下条件的职位将会被匹配</div>
<div>
<div flex flex-nowrap flex-items-start>
<template
v-for="(mGroup, index) in [
[12, 13, 14, 15, 16, 17, 18],
[19, 20, 21, 22, 23, 24]
]"
:key="index"
>
<table
:style="{
lineHeight: '1.25em'
}"
>
<tr>
<th
v-for="(text, i) in ['月薪下限', '月薪上限', '']"
:key="i"
:style="{
borderBottom: '2px solid #f0f0f0'
}"
>
{{ text }}
</th>
</tr>
<tr v-for="m in mGroup" :key="m">
<td>
{{
formContent.expectSalaryLow
? ((formContent.expectSalaryLow / m) * 10).toFixed(2)
: '无下限'
}}<small v-if="formContent.expectSalaryLow" class="color-#999 ml-2px"
>k</small
>
</td>
<td>
{{
formContent.expectSalaryHigh
? ((formContent.expectSalaryHigh / m) * 10).toFixed(2)
: '无上限'
}}<small v-if="formContent.expectSalaryHigh" class="color-#999 ml-2px"
>k</small
>
</td>
<td>{{ m }}薪</td>
</tr>
</table>
<div v-if="index !== 1" class="bg-#f0f0f0 w-2px flex-self-stretch"></div>
</template>
</div>
</div>
</div>
</el-form-item>
</div>
</div>
</div>
<div class="h-1px bg-#f0f0f0" mt16px mb16px />
<div>
<div
flex
:style="{
alignItems: 'center',
justifyContent: 'space-between'
}"
>
<div font-size-14px>期望职位信息</div>
<div>
<el-dropdown ml20px @command="handleExpectJobFilterTemplateClicked">
<el-button size="small"
>职位详情筛选模板(按职类区分)
<el-icon class="el-icon--right"><arrow-down /></el-icon
></el-button>
<template #dropdown>
<el-dropdown-menu
:style="{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr'
}"
>
<el-dropdown-item
v-for="item in expectJobFilterTemplateList"
:key="item.name"
:command="item"
>{{ item.name }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div
:style="{
width: '100%',
display: 'flex',
gap: '10px'
}"
>
<div flex-1>
<el-form-item mb0 prop="expectJobNameRegExpStr">
<div font-size-12px>职位名称/类型/描述 正则匹配筛选逻辑</div>
<el-select
v-model="formContent.jobDetailRegExpMatchLogic"
@change="(value) => gtagRenderer('job_detail_re_ml_change', { value })"
>
<el-option
v-for="op in jobDetailRegExpMatchLogicOptions"
:key="op.value"
:label="op.name"
:value="op.value"
>{{ op.name }}</el-option
>
</el-select>
</el-form-item>
<div
:style="{
width: '100%',
height: '1px',
backgroundColor: '#f0f0f0',
marginTop: '0.5lh'
}"
/>
<div
:style="{
display: 'grid',
gridTemplateColumns: '2em 1fr',
gap: '5px',
flex: 1,
alignItems: 'end'
}"
class="job-detail-filter-wrap"
>
<div></div>
<el-tooltip
effect="light"
placement="bottom"
@show="gtagRenderer('tooltip_show_about_how_to_fill_df')"
>
<template #content>
<div w-800px>
<div style="margin-top: 4px; margin-bottom: 4px">
上方“职位名称/类型/描述
正则匹配筛选逻辑”配置,你可以自行决定如下三个正则“所有正则匹配时才认为职位匹配”还是“任一正则匹配时即认为职位匹配”。
</div>
<ul m0>
<li>
当选择“所有正则匹配时才认为职位匹配”规则时,如果你留空某个输入框,表示任何职位一定匹配这个条件。
</li>
<li>
当选择“任一正则匹配时即认为职位匹配”规则时,如果你留空某个输入框,表示任何职位一定不匹配这个条件。
</li>
</ul>
<b>请注意</b>,如果<span color-orange>三个输入框均留空</span
>,无论上方“职位名称/类型/描述 正则匹配筛选逻辑”配置是什么,都表示<span
color-orange
>列表中出现的任意职位都将认为同时匹配这三个条件(即不根据职位名称/类型/描述进行筛选)</span
>。<br />
因此,可以按照如下场景填写你对于期望职位的筛选条件:
<ul style="margin-top: 4px; margin-bottom: 4px">
<li>
如果你只考虑工作类型,请填写“职位类型正则”输入框,其余两个输入框清空。这可以确保求职方向基本正确。
</li>
<li>
如果你着重关注职位描述,请填写“职位描述正则”,其余两个输入框酌情填写。
</li>
<li>
如果你想开聊列表里的推荐的任意职位,不根据职位名称/类型/描述进行筛选,请清空这三个输入框。
</li>
</ul>
<div>
你可以在右侧"职位详情筛选模板"选择一个模板,并在选中模板基础上尝试修改
</div>
<div>
<b>“职位类型正则”填写过程中请注意</b
>“职位类型”是由BOSS直聘预定义好的一系列职位分类因此<b>请按照这个分类来编写正则</b>。<br />
<!-- 这个分类可以在此找到:<br />
<img w-400px src="../resources/job-type-source-entry.png" /> -->
</div>
<div>
关于误伤/误投的排查,<a
href="javascript:;"
style="color: var(--el-color-primary)"
@click.prevent="handleHowToFillDetailFilterClick"
>请参阅这个链接</a
>
</div>
</div>
</template>
<el-button
type="text"
font-size-12px
:style="{
width: 'fit-content',
padding: '0',
height: 'auto',
position: 'relative',
top: '6px'
}"
><span><QuestionFilled w-1em h-1em mr2px /></span
>如下三个输入框工作机制是怎样的?怎样填写?误伤/误投如何排查?</el-button
>
</el-tooltip>
<div></div>
<el-form-item mb0 prop="expectJobNameRegExpStr">
<div font-size-12px>职位名称正则(不区分大小写)</div>
<el-input
v-model="formContent.expectJobNameRegExpStr"
type="textarea"
:placeholder="
getJobDetailRegExpMatchLogicConfig({ formContent }).inputPlaceholderText
"
:autosize="{ minRows: 2 }"
max-h-6lh
@blur="
formContent.expectJobNameRegExpStr =
formContent.expectJobNameRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
<div
mb0px
font-size-12px
flex
flex-justify-center
fw-800
flex-self-start
position-relative
style="top: 42px"
>
{{ getJobDetailRegExpMatchLogicConfig({ formContent }).logicText }}
</div>
<el-form-item mb0 prop="expectJobTypeRegExpStr">
<div ref="jobDetailRegExpSectionEl" font-size-12px>
职位类型正则(推荐填写,不区分大小写)
</div>
<el-input
v-model="formContent.expectJobTypeRegExpStr"
type="textarea"
:placeholder="
getJobDetailRegExpMatchLogicConfig({ formContent }).inputPlaceholderText
"
:autosize="{ minRows: 2 }"
max-h-6lh
@blur="
formContent.expectJobTypeRegExpStr =
formContent.expectJobTypeRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
<div
mb0px
font-size-12px
flex
flex-justify-center
fw-800
flex-self-start
position-relative
style="top: 42px"
>
{{ getJobDetailRegExpMatchLogicConfig({ formContent }).logicText }}
</div>
<el-form-item mb0 prop="expectJobDescRegExpStr">
<div font-size-12px>职位描述正则(不区分大小写)</div>
<el-input
v-model="formContent.expectJobDescRegExpStr"
type="textarea"
:placeholder="
getJobDetailRegExpMatchLogicConfig({ formContent }).inputPlaceholderText
"
:autosize="{ minRows: 2 }"
max-h-6lh
@blur="
formContent.expectJobDescRegExpStr =
formContent.expectJobDescRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
</div>
</div>
</div>
</div>
</el-form>
</div>
<div class="bg-#f8f8f8 pb10px pt10px">
<div
:style="{
display: 'flex',
justifyContent: 'end',
maxWidth: '800px',
margin: '0 auto',
paddingLeft: '20px',
paddingRight: 'calc(20px + 16px)'
}"
>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { JobDetailRegExpMatchLogic, SalaryCalculateWay } from '@geekgeekrun/sqlite-plugin/src/enums'
import CityChooser from '../MainLayout/GeekAutoStartChatWithBoss/components/CityChooser.vue'
import { QuestionFilled, ArrowDown } from '@element-plus/icons-vue'
import {
getJobDetailRegExpMatchLogicConfig,
expectSalaryCalculateWayOption,
ensureSalaryRangeCorrect,
expectCompanyTemplateList,
blockCompanyNameRegExpTemplateList,
getHandlerForBlockCompanyNameRegExpTemplateClicked,
getHandlerForExpectCompanyTemplateClicked,
getHandlerForExpectJobFilterTemplateClicked,
getRuleOfExpectJobNameRegExpStr,
getRuleOfExpectJobDescRegExpStr,
getRuleOfExpectJobTypeRegExpStr,
getRuleOfBlockCompanyNameRegExpStr,
jobDetailRegExpMatchLogicOptions,
getHandlerForExpectSalaryCalculateWayChanged,
normalizeCommaSplittedStr
} from '../MainLayout/GeekAutoStartChatWithBoss/common'
import { computed, ref } from 'vue'
import expectJobFilterTemplateList from '../MainLayout/GeekAutoStartChatWithBoss/expectJobFilterTemplateList'
const { ipcRenderer } = window.electron
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'cjc_config',
...params
})
}
const formContent = ref({
expectCompanies: '',
expectCityList: [],
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY,
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '',
expectJobDescRegExpStr: '',
expectSalaryCalculateWay: SalaryCalculateWay.ANNUAL_PACKAGE,
expectSalaryHigh: null,
expectSalaryLow: null,
blockCompanyNameRegExpStr: ''
})
const jobDetailRegExpSectionEl = ref<HTMLDivElement>()
const blockCompanyNameRegExpSectionEl = ref<HTMLDivElement>()
const formRules = computed(() => ({
expectJobNameRegExpStr: {
trigger: 'blur',
validator: getRuleOfExpectJobNameRegExpStr({ gtagRenderer, jobDetailRegExpSectionEl })
},
expectJobTypeRegExpStr: {
trigger: 'blur',
validator: getRuleOfExpectJobTypeRegExpStr({ gtagRenderer, jobDetailRegExpSectionEl })
},
expectJobDescRegExpStr: {
trigger: 'blur',
validator: getRuleOfExpectJobDescRegExpStr({ gtagRenderer, jobDetailRegExpSectionEl })
},
blockCompanyNameRegExpStr: {
trigger: 'blur',
validator: getRuleOfBlockCompanyNameRegExpStr({ gtagRenderer, blockCompanyNameRegExpSectionEl })
}
}))
const handleBlockCompanyNameRegExpTemplateClicked =
getHandlerForBlockCompanyNameRegExpTemplateClicked({
gtagRenderer,
formContent
})
const handleExpectCompanyTemplateClicked = getHandlerForExpectCompanyTemplateClicked({
gtagRenderer,
formContent
})
const handleExpectJobFilterTemplateClicked = getHandlerForExpectJobFilterTemplateClicked({
gtagRenderer,
formContent
})
function handleHowToFillDetailFilterClick() {
gtagRenderer('click_linux_do_how_to_fill_df')
electron.ipcRenderer.send(
'open-external-link',
'https://linux.do/t/topic/640626/74?u=geekgeekrun'
)
}
const handleExpectSalaryCalculateWayChanged = getHandlerForExpectSalaryCalculateWayChanged({
gtagRenderer,
formContent
})
function handleCancel() {
//
window.close()
}
const formRef = ref()
async function handleSave() {
await formRef.value?.validate()
await ipcRenderer.invoke(
'save-common-job-condition-config',
JSON.parse(
JSON.stringify({
...formContent.value,
expectCompanies: formContent.value.expectCompanies
? formContent.value.expectCompanies.split(',').map((s) => s.trim())
: []
})
)
)
ipcRenderer.send('common-job-condition-config-done')
}
ipcRenderer.invoke('fetch-config-file-content').then((res) => {
const commonJobConditionConfig = res.config?.['common-job-condition-config.json'] ?? {}
Object.keys(formContent.value).forEach((key) => {
if (key in commonJobConditionConfig) {
if (key === 'expectCompanies') {
formContent.value[key] = (commonJobConditionConfig[key] ?? []).join(',')
} else {
formContent.value[key] = commonJobConditionConfig[key]
}
}
})
})
</script>
<style lang="scss">
.common-job-condition-config .el-form-item__error.el-form-item__error--inline {
margin-left: 0;
}
</style>

View File

@@ -1,112 +1,118 @@
<template>
<div ref="componentRootEl" class="first-run-readme">
<div class="first-run-readme__inner">
<div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div>
<div class="readme-desc">
如下是使用必读请您逐条阅读如果已经了解且接受请在每一条前面打勾
</div>
<article class="readme-article">
<ElCheckboxGroup
v-model="readmeItemCheckStatusList"
@change="handleReadmeItemCheckStatusListChange"
>
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
本程序从某种程度上说属于辅助工具<el-link
color-blue
@click.stop.prevent="
() => {
electron.ipcRenderer.send(
'open-external-link',
'https://about.zhipin.com/agreement/'
)
gtagRenderer('view_boss_agreement_clicked')
}
"
>BOSS直聘用户协议</el-link
>2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款
<i>用户的平台使用义务</i><i>违约责任</i>
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录账号被限制使用账号被封禁</b
>等对您不利的风险因此使用本程序即意味着<b class="color-red">您愿意接受以上风险</b
>且如果相关风险发生<b class="color-red">需要自行承担相关后果本程序不负责</b>
</ElCheckbox>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊BOSS以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b
class="color-red"
>注意节制</b
>建议当天开聊次数用尽后隔几天再使用建议您<b class="color-red"
>注册一个本程序专用的新的BOSS直聘账号</b
>进行求职
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red"
>浏览器重复闪退重新启动</b
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
您所在公司可能会对您的计算机终端或网络进行<b color-red>监控</b>从而<b color-red
>审计跟踪</b
>您的行为上级 / HRBP 团队可能会从 IT
团队处获取到监控数据从而了解团队成员离职倾向如果您不希望上级 / HRBP
团队了解到您正在求职建议您<b color-red
>不要在您所在公司提供的计算机终端或网络上使用本程序</b
>
</ElCheckbox>
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
本程序尊重您的隐私<b color-red
>不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及上级 / HRBP
团队报告您的求职行为不会向猎头公司泄露您的信息</b
>
</ElCheckbox>
<div class="first-run-readme__inner-outer">
<div class="first-run-readme__inner">
<div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div>
<div class="readme-desc">
如下是使用必读请您逐条阅读如果已经了解且接受请在每一条前面打勾
</div>
<article class="readme-article">
<ElCheckboxGroup
v-model="readmeItemCheckStatusList"
@change="handleReadmeItemCheckStatusListChange"
>
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
本程序从某种程度上说属于辅助工具<el-link
color-blue
@click.stop.prevent="
() => {
electron.ipcRenderer.send(
'open-external-link',
'https://about.zhipin.com/agreement/'
)
gtagRenderer('view_boss_agreement_clicked')
}
"
>BOSS直聘用户协议</el-link
>2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款
<i>用户的平台使用义务</i><i>违约责任</i>
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录账号被限制使用账号被封禁</b
>等对您不利的风险因此使用本程序即意味着<b class="color-red">愿意接受以上风险</b
>且如果相关风险发生<b class="color-red">您需要自行承担相关后果本程序不负责</b>
</ElCheckbox>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊BOSS以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b
class="color-red"
>注意节制</b
>建议当天开聊次数用尽后隔几天再使用建议您<b class="color-red"
>注册一个本程序专用的新的BOSS直聘账号</b
>进行求职
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red"
>浏览器重复闪退重新启动</b
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
您所在公司可能会采购上网行为监控工具或网关例如奇安信深信服绿盟等厂商的产品对您的计算机终端或网络进行<b
color-red
>监控</b
>从而<b color-red>审计跟踪</b>您的行为您的上级/IT/HR
可能会获取到监控数据从而了解团队成员离职倾向如果您不希望您的上级/IT/HR
了解到您正在求职建议您<b color-red
>不要在您所在公司提供的计算机终端或网络上使用本程序</b
>
</ElCheckbox>
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
本程序尊重您的隐私<b color-red
>不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及您的上级/IT/HR报告您的求职行为不会向猎头公司泄露您的信息</b
>但由于本程序开源<b color-red
>任何人均可更改本程序源码并重新发布这一过程中其它开发者是可以加入恶意程序的因此请从你信任的源下载本程序</b
>
</ElCheckbox>
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
本程序<b class="color-red">没有内置任何付费功能</b><b class="color-red"
>下载使用是免费的</b
>任何人可以<b class="color-red">免费获得免费使用</b><b class="color-red"
>作者没有利用本程序赚到过任何收入</b
>如果您是从GitHub以外的地方付费后购买的本程序或您被提示必须付费后才能使用本程序<b
class="color-red"
>那您大概率被骗了</b
>或者<b class="color-red">您下载到了本程序修改版</b><b class="color-red"
>本程序对此概不负责请勿找作者商讨退款售后事宜相关事宜请咨询卖方</b
>
</ElCheckbox>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责<b
class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
>
</ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃<b
class="color-red"
>在BOSS直聘上屏蔽当前公司及与之关联的公司</b
>
</ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过
VMware Workstation / FusionOracle VirtualBoxMicrosoft Hyper-V
等虚拟化技术运行本程序如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
</ElCheckboxGroup>
</article>
<footer flex mt20px pb20px flex-justify-end>
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
本程序<b class="color-red">没有内置任何付费功能</b><b class="color-red"
>下载使用是免费的</b
>任何人可以<b class="color-red">免费获得免费使用</b><b class="color-red"
>作者没有利用本程序赚到过任何收入</b
>如果您是从GitHub以外的地方付费后购买的本程序或您被提示必须付费后才能使用本程序<b
class="color-red"
>那您大概率被骗了</b
>或者<b class="color-red">您下载到了本程序修改版</b><b class="color-red"
>本程序对此概不负责请勿找作者商讨退款售后事宜相关事宜请咨询卖方</b
>
</ElCheckbox>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责<b
class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
>
</ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃<b
class="color-red"
>在BOSS直聘上屏蔽当前公司及与之关联的公司</b
>
</ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过
VMware Workstation / FusionOracle VirtualBoxMicrosoft Hyper-V
等虚拟化技术运行本程序如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
</ElCheckboxGroup>
</article>
</div>
</div>
<footer pt10px pb10px>
<div flex flex-justify-end w-880px ml-auto mr-auto>
<el-button type="text" @click="handleCancel">退出程序</el-button>
<el-button type="primary" @click="handleSubmit"
>我已经阅读并接受上方所提及的相关风险并决定继续使用本程序</el-button
>
</footer>
</div>
</div>
</footer>
</div>
</template>
@@ -161,30 +167,27 @@ const handleReadmeItemCheckStatusListChange = (value: number[]) => {
})
}
const componentRootEl = ref<HTMLElement>()
onMounted(() => {
const ro = new ResizeObserver(() => {
electron.ipcRenderer.send('update-window-size', {
width: componentRootEl.value!.offsetWidth,
height: componentRootEl.value!.offsetHeight
})
})
ro.observe(componentRootEl.value!)
onBeforeMount(() => {
ro.disconnect()
})
})
</script>
<style lang="scss" scoped>
.first-run-readme {
box-sizing: border-box;
width: 960px;
height: fit-content;
width: 100vw;
height: 100vh;
overflow: auto;
user-select: none;
&__inner {
display: flex;
flex-direction: column;
overflow: hidden;
.first-run-readme__inner-outer {
flex: 1;
overflow: auto;
}
.first-run-readme__inner {
width: 880px;
margin: 0 auto;
padding-top: 30px;
padding-bottom: 30px;
.readme-title {
}
.readme-desc {
@@ -220,5 +223,9 @@ onMounted(() => {
}
}
}
footer {
flex: 0;
background-color: #f0f0f0;
}
}
</style>

View File

@@ -0,0 +1,282 @@
import { SalaryCalculateWay, JobDetailRegExpMatchLogic } from '@geekgeekrun/sqlite-plugin/src/enums'
import sampleCompanyList from '@geekgeekrun/geek-auto-start-chat-with-boss/default-config-file/sample-company-list.json'
import { nextTick } from 'vue'
export function isJobDetailRegExpEmpty({ formContent }) {
return [
formContent.expectJobDescRegExpStr,
formContent.expectJobNameRegExpStr,
formContent.expectJobTypeRegExpStr
]
.map((it) => Boolean(it?.trim()))
.every((it) => it === false)
}
export function getJobDetailRegExpMatchLogicConfig({ formContent }) {
const result = {
logicText: '-',
inputPlaceholderText: '-'
}
if (formContent.jobDetailRegExpMatchLogic === JobDetailRegExpMatchLogic.EVERY) {
Object.assign(result, {
logicText: '且',
inputPlaceholderText: 'true'
})
}
if (formContent.jobDetailRegExpMatchLogic === JobDetailRegExpMatchLogic.SOME) {
Object.assign(result, {
logicText: '或',
inputPlaceholderText: 'false'
})
}
if (isJobDetailRegExpEmpty({ formContent })) {
result.inputPlaceholderText = 'true'
}
return result
}
export const expectSalaryCalculateWayOption = [
{
name: '月薪(单位为 千元 - 即“k”',
value: SalaryCalculateWay.MONTH_SALARY
},
{
name: '总包(单位为 万元 - 即“W”',
value: SalaryCalculateWay.ANNUAL_PACKAGE
}
]
export function ensureSalaryRangeCorrect({ formContent }) {
if (!formContent.expectSalaryHigh || isNaN(parseFloat(formContent.expectSalaryHigh))) {
formContent.expectSalaryHigh = null
} else {
formContent.expectSalaryHigh = parseFloat(formContent.expectSalaryHigh.toFixed(2))
}
if (!formContent.expectSalaryLow || isNaN(parseFloat(formContent.expectSalaryLow))) {
formContent.expectSalaryLow = null
} else {
formContent.expectSalaryLow = parseFloat(formContent.expectSalaryLow.toFixed(2))
}
if (
formContent.expectSalaryLow &&
formContent.expectSalaryHigh &&
formContent.expectSalaryLow > formContent.expectSalaryHigh
) {
formContent.expectSalaryHigh = formContent.expectSalaryLow
}
}
export function getRuleOfExpectJobNameRegExpStr({ gtagRenderer, jobDetailRegExpSectionEl }) {
return (_, value, cb) => {
if (!value) {
cb()
gtagRenderer('empty_reg_exp_for_expect_job_name')
return
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_name', { v: value })
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_name', { v: value })
}
}
}
export function getRuleOfExpectJobTypeRegExpStr({ gtagRenderer, jobDetailRegExpSectionEl }) {
return (_, value, cb) => {
if (!value) {
cb()
gtagRenderer('empty_reg_exp_for_expect_job_type')
return
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_type', { v: value })
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_type', { v: value })
}
}
}
export function getRuleOfExpectJobDescRegExpStr({ gtagRenderer, jobDetailRegExpSectionEl }) {
return (_, value, cb) => {
if (!value) {
cb()
gtagRenderer('empty_reg_exp_for_expect_job_desc')
return
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_expect_job_desc', { v: value })
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_desc', { v: value })
}
}
}
export function getRuleOfBlockCompanyNameRegExpStr({
gtagRenderer,
blockCompanyNameRegExpSectionEl
}) {
return (_, value, cb) => {
if (!value) {
cb()
gtagRenderer('empty_reg_exp_for_bcn')
return
}
try {
new RegExp(value, 'ig')
gtagRenderer('valid_reg_exp_for_bcn', { v: value })
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
blockCompanyNameRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_bcn', { v: value })
}
}
}
export const expectCompanyTemplateList = [
{
name: '不限公司(随便投)',
value: ''
},
{
name: '示例公司',
value: sampleCompanyList.join(',')
},
{
name: '大厂及关联企业',
value: `抖音,字节,字跳,有竹居,脸萌,头条,懂车帝,滴滴,嘀嘀,巨量引擎,小桔,网易,有道,腾讯,酷狗,酷我,阅文,搜狗,小鹅通,富途,京东,沃东天骏,达达,达冠,京邦达,百度,昆仑芯,小度,度小满,爱奇艺,携程,趣拿,去哪儿,集度,智图,长地万方,瑞图万方,道道通,小熊博望,理想,蔚来,顺丰,丰巢,中通,圆通,申通,跨越,讯飞,同程,艺龙,马蜂窝,贝壳,自如,链家,我爱我家,相寓,多点,金山,小米,猎豹,新浪,微博,阿里,淘宝,淘麦郎,天猫,盒马,口碑,优视,夸克,UC,蚂蚁,高德,LAZADA,来赞达,飞猪,菜鸟,哈啰,钉钉,乌鸫,饿了么,美团,三快,猫眼,快手,映客,小红书,行吟,奇虎,360,三六零,鸿盈,奇富,奇元,亚信,启明星辰,奇安信,深信服,长亭,绿盟,天融信,商汤,SenseTime,大华,海康威视,hikvision,汽车之家,车好多,瓜子,易车,昆仑万维,昆仑天工,闲徕,趣加,FunPlus,完美,马上消费,轻松,水滴,白龙马,58,更赢,车欢欢,五八,红布林,致美,快狗,天鹅到家,转转,美餐,知乎,智者四海,易点云,搜狐,用友,畅捷通,猿辅导,小猿,猿力,好未来,学而思,希望学,新东方,东方甄选,东方优选,作业帮,高途,跟谁学,学科网,天学网,一起教育,一起作业,美术宝,火花思维,粉笔,51talk,爱学习,高思,老虎国际,一心向上,向上一意,联想,拉勾,乐视,欢聚,竞技世界,拼多多,寻梦,从鲸,TEMU,得物,有赞,Moka,希瑞亚斯,北森,OPPO,欧珀,vivo,维沃,小天才,步步高,读书郎,货拉拉,陌陌,探探,Shopee,虾皮,首汽租车,GoFun,神州租车,天眼查,旷视,小冰,美图,智谱华章,MiniMax,石头科技,迅雷,TP,锐捷,Tenda,腾达,斐讯,希音,SHEIN,稀宇,深言,百川智能,与爱为舞,牵手,Grab,爱回收,洋钱罐,瓴岳,得到,思维造物,地平线,咪咕,翼支付,电信,天翼,联通,蓝湖,墨刀,海尔,美的,米哈游,传音,同花顺,国美,TCL`
},
{
name: '阿里系',
value: `阿里,淘宝,淘麦郎,天猫,盒马,口碑,优视,夸克,UC,蚂蚁,飞猪,乌鸫,饿了么,LAZADA,来赞达,菜鸟,哈啰,钉钉,高德,白龙马,新浪,微博`
},
{
name: '字节(头条/抖音)系',
value: `抖音,字节,字跳,有竹居,脸萌,头条,懂车帝,巨量引擎`
},
{
name: '百度系',
value: `百度,昆仑芯,小度,度小满,爱奇艺,携程,趣拿,去哪儿,集度,作业帮,智图,长地万方,瑞图万方,道道通,小熊博望`
},
{
name: '腾讯系',
value: `腾讯,酷狗,酷我,阅文,搜狗,小鹅通,富途,京东,沃东天骏,达达,达冠,京邦达,美团,三快,猫眼,快手,拼多多,寻梦,从鲸,TEMU,Shopee,虾皮,滴滴,嘀嘀,小桔,转转`
},
{
name: '外包、劳务派遣企业',
value: `青钱,软通动力,南天,睿服,中电金信,佰钧成,云链,博彦,汉克时代,柯莱特,拓保,亿达信息,纬创,微创,微澜,诚迈科技,法本,兆尹,诚迈,联合永道,新致软件,宇信科技,华为,德科,FESCO,科锐,科之锐`
}
]
export const blockCompanyNameRegExpTemplateList = [
{
name: '不限公司(不按照公司名称来标注不合适)',
value: ''
},
{
name: '外包、劳务派遣企业',
value: `青钱|软通动力|南天|睿服|中电金信|佰钧成|云链|博彦|汉克时代|柯莱特|拓保|亿达信息|纬创|微创|微澜|诚迈科技|法本|兆尹|诚迈|联合永道|新致软件|宇信科技|华为|德科|FESCO|科锐|科之锐`
},
{
name: '京东及相关公司',
value: '京东|沃东天骏|达达|达冠|京邦达'
}
]
export function getHandlerForExpectCompanyTemplateClicked({ gtagRenderer, formContent }) {
return function handleExpectCompanyTemplateClicked(item) {
gtagRenderer('expect_company_tpl_clicked', {
name: item.name
})
formContent.value.expectCompanies = item.value
}
}
export function getHandlerForExpectJobFilterTemplateClicked({ gtagRenderer, formContent }) {
return function handleExpectJobFilterTemplateClicked(item) {
gtagRenderer('expect_job_filter_tpl_clicked', {
name: item.name
})
Object.assign(formContent.value, {
...item.config
})
}
}
export function getHandlerForBlockCompanyNameRegExpTemplateClicked({ gtagRenderer, formContent }) {
return function handleBlockCompanyNameRegExpTemplateClicked(item) {
gtagRenderer('bcn_reg_exp_tpl_clicked', {
name: item.name
})
formContent.value.blockCompanyNameRegExpStr = item.value
}
}
export const jobDetailRegExpMatchLogicOptions = [
{
name: '“且”模式 - 所有正则匹配时才认为职位匹配',
value: JobDetailRegExpMatchLogic.EVERY
},
{
name: '“或”模式 - 任一正则匹配时即认为职位匹配',
value: JobDetailRegExpMatchLogic.SOME
}
]
export function getHandlerForExpectSalaryCalculateWayChanged({ gtagRenderer, formContent }) {
return async function handleExpectSalaryCalculateWayChanged(value) {
gtagRenderer('expect_salary_calculate_way_changed', { value })
await nextTick()
// convert annual package to month salary as 12-month
if (value === SalaryCalculateWay.MONTH_SALARY) {
if (formContent.value.expectSalaryHigh) {
formContent.value.expectSalaryHigh = Number(
((formContent.value.expectSalaryHigh * 10) / 12).toFixed(2)
)
}
if (formContent.value.expectSalaryLow) {
formContent.value.expectSalaryLow = Number(
((formContent.value.expectSalaryLow * 10) / 12).toFixed(2)
)
}
return
}
// convert month salary to annual package as 12-month
else if (value === SalaryCalculateWay.ANNUAL_PACKAGE) {
if (formContent.value.expectSalaryHigh) {
formContent.value.expectSalaryHigh = Number(
((formContent.value.expectSalaryHigh / 10) * 12).toFixed(2)
)
}
if (formContent.value.expectSalaryLow) {
formContent.value.expectSalaryLow = Number(
((formContent.value.expectSalaryLow / 10) * 12).toFixed(2)
)
}
return
}
}
}
export const normalizeCommaSplittedStr = (str) => {
return str
.split(/,|/)
.map((it) => it.trim())
.filter(Boolean)
.join(',')
}

View File

@@ -0,0 +1,128 @@
<template>
<div class="group-item">
<div class="group-title">逛BOSS</div>
<div flex flex-col class="link-list">
<RouterLink to="./GeekAutoStartChatWithBoss">
自动开聊
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
>
<template #content>
<div w-480px>
<div>扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的BOSS</div>
<br />
<div>匹配步骤</div>
<ol m0 pl2em>
<li>
按照公司名称查找职位查找到目标职位后自动点击这个职位右侧将会展示职位详情
</li>
<li>
检查BOSS活跃度
<ul pl2em>
<li>
如果BOSS活跃度为本月活跃或更往前的时间则会把职位标记为不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位
</li>
</ul>
</li>
<li>
对职位名称职位类型职位描述进行匹配
<ul pl2em>
<li>如果匹配则自动点击开聊按钮</li>
<li>
不匹配则标记这个职位为不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位
</li>
</ul>
</li>
</ol>
<br />
<div>异常情况</div>
<ol m0 pl2em>
<li>
当前页面筛选条件下如果没有更多职位则自动切换备选筛选条件以获取更多新职位
</li>
<li>
如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
</li>
</ol>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<RouterLink to="./ReadNoReplyReminder">
已读不回自动复聊
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div w-480px>
<div>
BOSS不明原因已读不回简历就是投不出去<br />
已读不回自动复聊提醒一下已读不回的 BOSS助力把握每次机会
</div>
<br />
<div>匹配逻辑</div>
<div>在聊天列表中查找对你消息已读不回的BOSS再发一条消息多次复聊同时</div>
<ul m0 pl2em>
<li>如果设置了跟进时限那么在这个时间之前活跃的聊天将不会被检查</li>
<li>
如果设置了跟进间隔且再次检查时发现BOSS已读不回且距离上次提醒时间间隔小于这个时间那么聊天将暂时不会跟进直到下次检查时距离上次提醒时间间隔大于这个时间
</li>
</ul>
<br />
<div>发送内容</div>
<ul m0 pl2em>
<li>[盼回复]表情</li>
<li>由大语言模型根据简历及当前聊天上下文生成的内容</li>
</ul>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<a href="javascript:void(0)" @click="handleClickLaunchBossLogin">
编辑登录凭据<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleLaunchBossSite">
手动逛<TopRight w-1em h-1em mr10px />
</a>
</div>
</div>
</template>
<script lang="ts" setup>
import { gtagRenderer } from '@renderer/utils/gtag'
import { debounce } from 'lodash'
import { ElMessage } from 'element-plus'
import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
const handleClickLaunchBossLogin = async () => {
gtagRenderer('launch_login_clicked')
try {
await electron.ipcRenderer.invoke('login-with-cookie-assistant')
ElMessage({
type: 'success',
message: '登录凭据保存成功'
})
} catch {
//
}
}
const handleLaunchBossSite = debounce(
async () => {
gtagRenderer('launch_boss_site_clicked')
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
url: `https://www.zhipin.com/`
})
},
1000,
{ leading: true, trailing: false }
)
</script>
<style scoped lang="scss" src="./style.scss"></style>

View File

@@ -0,0 +1,86 @@
<template>
<div class="group-item">
<div class="group-title">全局设置</div>
<div flex flex-col class="link-list">
<a href="javascript:void(0)" @click="handleClickConfigCommonJobCondition">
公共职位筛选条件
</a>
<a href="javascript:void(0)" @click="handleClickBrowserSetting">
编辑浏览器偏好<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleClickConfigLlm">
配置大语言模型
<div>
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div class="font-size-12px">
支持
<span
class="pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #3c4efd"
>DeepSeek-V3</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-black border-rd-full font-size-0.8em"
style="background-color: #fff"
>GPT-4o mini</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #462ac4"
>Qwen2.5</span
>
模型<br />支持多个服务商-模型组合按权重搭配使用
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
<TopRight w-1em h-1em mr10px />
</div>
</a>
</div>
</div>
</template>
<script lang="ts" setup>
import { gtagRenderer } from '@renderer/utils/gtag'
import { ElMessage } from 'element-plus'
import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
const handleClickBrowserSetting = async () => {
gtagRenderer('browser_setting_clicked')
try {
await electron.ipcRenderer.invoke('config-with-browser-assistant')
ElMessage({
type: 'success',
message: '浏览器偏好保存成功'
})
} catch {
//
}
}
const handleClickConfigLlm = async () => {
gtagRenderer('config_llm_clicked')
try {
await electron.ipcRenderer.invoke('llm-config')
} catch (err) {
console.log(err)
}
}
const handleClickConfigCommonJobCondition = async () => {
gtagRenderer('config_cjc_clicked', { entry: 'left-nav' })
try {
await electron.ipcRenderer.invoke('common-job-condition-config')
} catch (err) {
console.log(err)
}
}
</script>
<style scoped lang="scss" src="./style.scss"></style>

View File

@@ -0,0 +1,14 @@
<template>
<div class="group-item">
<div class="group-title">运行数据</div>
<div flex flex-col class="link-list">
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
<RouterLink to="./JobLibrary">职位库</RouterLink>
<RouterLink to="./BossLibrary">BOSS库</RouterLink>
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
</div>
</div>
</template>
<style scoped lang="scss" src="./style.scss"></style>

View File

@@ -0,0 +1,26 @@
.group-item {
.group-title {
color: #849492;
font-size: 12px;
padding: 0.25em 0;
}
.link-list {
a {
display: flex;
align-items: center;
justify-content: space-between;
height: 2em;
box-sizing: border-box;
padding-left: 1em;
font-size: 14px;
&.router-link-active {
background-color: #fff;
font-weight: 700;
color: #2faa9e;
border-radius: 9999px 0 0 9999px;
position: relative;
box-shadow: 0px 0px 10px rgba(50, 114, 108, 0.187);
}
}
}
}

View File

@@ -9,7 +9,11 @@
>
<el-form-item>
<div>
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
<el-checkbox
v-if="!expectJobTypeRegExpStrForRender?.trim()"
:model-value="false"
disabled
>
发送提醒消息前先按照自动开聊-职位类型正则校验正在与BOSS沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<template v-else>
@@ -17,7 +21,7 @@
发送提醒消息前先按照自动开聊-职位类型正则校验正在与BOSS沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前职位类型正则{{ expectJobTypeRegExpStr?.trim() }}</div>
<div>当前职位类型正则{{ expectJobTypeRegExpStrForRender?.trim() }}</div>
<template
v-if="
formContent.autoReminder.rechatContentSource ===
@@ -33,7 +37,11 @@
</el-form-item>
<el-form-item>
<div>
<el-checkbox v-if="!blockCompanyNameRegExpStr?.trim()" :model-value="false" disabled>
<el-checkbox
v-if="!blockCompanyNameRegExpStrForRender?.trim()"
:model-value="false"
disabled
>
发送提醒消息前,先按照“自动开聊-不期望投递公司正则”校验正在与BOSS沟通的岗位是否归属于不期望投递的公司如果是则不提醒
</el-checkbox>
<template v-else>
@@ -41,12 +49,82 @@
发送提醒消息前,先按照“自动开聊-不期望投递公司正则”校验正在与BOSS沟通的岗位是否归属于不期望投递的公司如果是则不提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前不期望投递公司正则:{{ blockCompanyNameRegExpStr?.trim() }}</div>
<div>当前不期望投递公司正则:{{ blockCompanyNameRegExpStrForRender?.trim() }}</div>
</div>
</template>
</div>
</el-form-item>
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的BOSS时将要向BOSS发出">
<el-form-item label="开场白话术">
<el-radio-group v-model="formContent.autoReminder.openContentSource" w-full>
<div w-full>
<el-radio :label="OPEN_CONTENT_SOURCE.CONSTANT_CONTENT"> 固定文案 </el-radio>
<el-input
v-if="
formContent.autoReminder.openContentSource ===
OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
"
v-model="formContent.autoReminder.constantOpenContent"
w-full
:autosize="{ minRows: 3 }"
max-h-8lh
type="textarea"
:placeholder="defaultConstantOpenContent"
class="pl-30px mb10px"
:style="{
boxSizing: 'border-box'
}"
/>
</div>
<div>
<el-radio :label="OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型生成的内容
</el-radio>
<div
v-if="
formContent.autoReminder.openContentSource ===
OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
ml30px
>
<el-form-item class="mb4px">
<div>
<div>
<el-button
size="small"
type="primary"
@click="
handleClickEditPrompt({
type: 'open'
})
"
>
使用外部编辑器编辑“开场白话术”提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
restoreDefaultTemplate({
type: 'open',
gaEvName: 'reset_template_clicked_in_main_form'
})
}
"
>
还原默认“开场白话术”提示词模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑“开场白话术”提示词模板。
</div>
</div>
</el-form-item>
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item label="跟进话术 - 当发现已读不回的BOSS时将要向BOSS发出">
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
<div>
<el-tooltip
@@ -63,91 +141,117 @@
</el-radio>
</el-tooltip>
<br />
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
<div>
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
<div
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
ml30px
>
<el-form-item class="mb4px">
<div>
<div>
<el-button
size="small"
type="primary"
@click="
handleClickEditPrompt({
type: 'rechat'
})
"
>
使用外部编辑器编辑“跟进话术”提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
restoreDefaultTemplate({
type: 'rechat',
gaEvName: 'reset_template_clicked_in_main_form'
})
}
"
>
还原默认“跟进话术”提示词模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑“跟进话术”提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
</div>
</div>
</div>
</el-radio-group>
</el-form-item>
<div class="ml-30px">
<div mt10px>
<el-form-item mb0 label="大语言模型公共设置及效果预览" />
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
[
formContent.autoReminder.rechatContentSource,
formContent.autoReminder.openContentSource
].includes(RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT)
"
>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<div ml-30px>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑提示词模板 (Markdown)
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
gtagRenderer('reset_template_clicked_in_main_form')
restoreDefaultTemplate()
}
"
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item label="当配置的所有大模型均不可使用时">
<div class="flex flex-items-center">
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
>
还原默认提示词模板
</el-button>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回自动复聊过程</el-button
>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div class="flex flex-items-center">
<span class="whitespace-nowrap">当所有模型均不可使用时&nbsp;</span>
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
</div>
</el-form-item>
</el-form-item>
</div>
</template>
<el-form-item ml-30px>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回自动复聊过程</el-button
><span text-orange ml10px
>&lt;- 正式运行前建议在这里先试一试大模型生成效果是否符合预期哦</span
>
</el-form-item>
</div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
<el-input-number
@@ -251,6 +355,8 @@ import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { dayjs, ElForm, ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus'
import { useRouter } from 'vue-router'
import {
OPEN_CONTENT_SOURCE,
// OPEN_LLM_FALLBACK,
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK,
RUNNING_STATUS_ENUM
@@ -259,6 +365,7 @@ import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import mittBus from '../../utils/mitt'
import { QuestionFilled } from '@element-plus/icons-vue'
import RunningOverlay from '@renderer/features/RunningOverlay/index.vue'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../../common/constant'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'rnrr-config',
@@ -274,7 +381,10 @@ const formContent = ref({
recentMessageQuantityForLlm: 8,
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION,
onlyRemindBossWithExpectJobType: true,
onlyRemindBossWithoutBlockCompanyName: true
onlyRemindBossWithoutBlockCompanyName: true,
openContentSource: OPEN_CONTENT_SOURCE.CONSTANT_CONTENT,
// openLlmFallback: OPEN_LLM_FALLBACK.SEND_CONSTANT_CONTENT,
constantOpenContent: ''
}
})
@@ -307,16 +417,22 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
: parseInt(conf.recentMessageQuantityForLlm)
: 8
conf.onlyRemindBossWithExpectJobType = conf.onlyRemindBossWithExpectJobType ?? true
conf.onlyRemindBossWithoutBlockCompanyName = conf.onlyRemindBossWithoutBlockCompanyName ?? true
conf.rechatLlmFallback = conf.rechatLlmFallback ?? RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
conf.openContentSource = conf.openContentSource ?? OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
conf.constantOpenContent = conf.constantOpenContent ?? ''
formContent.value.autoReminder = conf
})
const expectJobTypeRegExpStr = ref('')
const blockCompanyNameRegExpStr = ref('')
const fieldsForUseCommonConfig = ref({})
async function fetchAutoStartChatConfig() {
await electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
expectJobTypeRegExpStr.value = res.config['boss.json']?.expectJobTypeRegExpStr
blockCompanyNameRegExpStr.value = res.config['boss.json']?.blockCompanyNameRegExpStr
fieldsForUseCommonConfig.value = res.config['boss.json']?.fieldsForUseCommonConfig ?? {}
commonJobConditionConfig.value = res.config['common-job-condition-config.json']
})
}
fetchAutoStartChatConfig()
@@ -324,6 +440,30 @@ mittBus.on('auto-start-chat-with-boss-config-saved', fetchAutoStartChatConfig)
onUnmounted(() => {
mittBus.off('auto-start-chat-with-boss-config-saved', fetchAutoStartChatConfig)
})
const commonJobConditionConfig = ref({})
const unListenCommonJobConditionConfig = electron.ipcRenderer.on(
'common-job-condition-config-updated',
(_, { config }) => {
commonJobConditionConfig.value = {
...config,
expectCompanies: config?.expectCompanies?.map((it) => it.trim())?.join(',') ?? ''
}
}
)
onUnmounted(() => {
unListenCommonJobConditionConfig()
})
const expectJobTypeRegExpStrForRender = computed(() => {
return !fieldsForUseCommonConfig.value.jobDetail
? expectJobTypeRegExpStr.value
: commonJobConditionConfig.value.expectJobTypeRegExpStr
})
const blockCompanyNameRegExpStrForRender = computed(() => {
return !fieldsForUseCommonConfig.value.blockCompanyNameRegExpStr
? blockCompanyNameRegExpStr.value
: commonJobConditionConfig.value.blockCompanyNameRegExpStr
})
const resumeContent = ref(null)
async function fetchResumeContent() {
@@ -427,8 +567,9 @@ async function checkIsCanRun() {
return false
}
try {
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid')
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid', { type: 'rechat' })
} catch (err) {
console.log(err)
if (err?.message?.includes(`RESUME_PLACEHOLDER_NOT_EXIST`)) {
gtagRenderer('cannot_launch_for_no_resume_placehold')
console.log(`提示词模板无效`, err)
@@ -444,8 +585,10 @@ async function checkIsCanRun() {
}
)
.then(async () => {
gtagRenderer('confirm_invalid_rt_tip_dialog')
await restoreDefaultTemplate()
await restoreDefaultTemplate({
type: 'rechat',
gaEvName: 'confirm_invalid_rt_tip_dialog'
})
})
.catch(() => {
gtagRenderer('close_invalid_rt_tip_dialog')
@@ -469,14 +612,23 @@ const handleSubmit = async () => {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
rechat_limit_day: formContent.value.autoReminder.rechatLimitDay,
rechat_content_source: formContent.value.autoReminder.rechatContentSource,
recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm
recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm,
only_remind_boss_with_expect_job_type:
formContent.value.autoReminder.onlyRemindBossWithExpectJobType,
only_remind_boss_without_block_company_name:
formContent.value.autoReminder.onlyRemindBossWithoutBlockCompanyName,
rechat_llm_fallback: formContent.value.autoReminder.rechatLlmFallback,
open_content_source: formContent.value.autoReminder.openContentSource,
constant_open_content_text_length: formContent.value.autoReminder.constantOpenContent.length ?? 0
})
await formRef.value!.validate()
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
gtagRenderer('config_saved')
if (
formContent.value.autoReminder?.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT ||
formContent.value.autoReminder?.openContentSource ===
OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) {
if (!(await checkIsCanRun())) {
return
@@ -533,8 +685,9 @@ function handleThrottleIntervalMinutesBlur() {
)
}
const restoreDefaultTemplate = async () => {
await electron.ipcRenderer.invoke('overwrite-auto-remind-prompt-with-default')
const restoreDefaultTemplate = async ({ type, gaEvName }) => {
gtagRenderer(gaEvName)
await electron.ipcRenderer.invoke('overwrite-auto-remind-prompt-with-default', { type })
ElMessage({
type: 'success',
message: '模板还原成功'
@@ -568,11 +721,12 @@ const handleClickEditResume = async () => {
}
}
const handleClickEditPrompt = async () => {
gtagRenderer('edit_prompt_clicked')
await electron.ipcRenderer.send('no-reply-reminder-prompt-edit')
const handleClickEditPrompt = async ({ type }) => {
gtagRenderer('edit_prompt_clicked', { type })
await electron.ipcRenderer.send('no-reply-reminder-prompt-edit', { type })
}
// for 跟进话术
const rechatLlmFallbackOptions = [
{
name: '发送“[盼回复]”表情',
@@ -584,6 +738,18 @@ const rechatLlmFallbackOptions = [
}
]
// // for 开场白话术
// const openLlmFallbackOptions = [
// {
// name: '发送固定文案',
// value: OPEN_LLM_FALLBACK.SEND_CONSTANT_CONTENT
// },
// {
// name: '退出已读不回自动复聊',
// value: OPEN_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
// }
// ]
async function handleTestEffectClicked() {
gtagRenderer('goto_mock_chat_clicked')
if (!(await checkIsCanRun())) {
@@ -616,6 +782,16 @@ const handleStopButtonClick = async () => {
isStopButtonLoading.value = false
}
}
const defaultConstantOpenContent = computed(() => {
if (
formContent.value.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join('')
}
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
})
</script>
<style lang="scss">

View File

@@ -3,153 +3,11 @@
<div class="flex flex-col min-w200px w200px pt30px pl30px aside-nav of-hidden">
<div class="nav-list flex-1 of-auto pl20px ml--20px">
<RouterLink v-show="false" to="./TaskManager">任务管理</RouterLink>
<div class="group-item">
<div class="group-title">BOSS直聘</div>
<div flex flex-col class="link-list">
<RouterLink to="./GeekAutoStartChatWithBoss">
自动开聊
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
>
<template #content>
<div w-480px>
<div>扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的BOSS</div>
<br />
<div>匹配步骤</div>
<ol m0 pl2em>
<li>
按照公司名称查找职位查找到目标职位后自动点击这个职位右侧将会展示职位详情
</li>
<li>
检查BOSS活跃度
<ul pl2em>
<li>
如果BOSS活跃度为本月活跃或更往前的时间则会把职位标记为不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位
</li>
</ul>
</li>
<li>
对职位名称职位类型职位描述进行匹配
<ul pl2em>
<li>如果匹配则自动点击开聊按钮</li>
<li>
不匹配则标记这个职位为不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位
</li>
</ul>
</li>
</ol>
<br />
<div>异常情况</div>
<ol m0 pl2em>
<li>
当前页面筛选条件下如果没有更多职位则自动切换备选筛选条件以获取更多新职位
</li>
<li>
如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
</li>
</ol>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<RouterLink to="./ReadNoReplyReminder">
已读不回自动复聊
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div w-480px>
<div>
BOSS不明原因已读不回简历就是投不出去<br />
已读不回自动复聊提醒一下已读不回的 BOSS助力把握每次机会
</div>
<br />
<div>匹配逻辑</div>
<div>在聊天列表中查找对你消息已读不回的BOSS再发一条消息多次复聊同时</div>
<ul m0 pl2em>
<li>如果设置了跟进时限那么在这个时间之前活跃的聊天将不会被检查</li>
<li>
如果设置了跟进间隔且再次检查时发现BOSS已读不回且距离上次提醒时间间隔小于这个时间那么聊天将暂时不会跟进直到下次检查时距离上次提醒时间间隔大于这个时间
</li>
</ul>
<br />
<div>发送内容</div>
<ul m0 pl2em>
<li>[盼回复]表情</li>
<li>由大语言模型根据简历及当前聊天上下文生成的内容</li>
</ul>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<a href="javascript:void(0)" @click="handleClickLaunchBossLogin">
编辑登录凭据<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleLaunchBossSite">
手动逛<TopRight w-1em h-1em mr10px />
</a>
</div>
</div>
<BossPart />
<hr class="group-divider" />
<div class="group-item">
<div class="group-title">全局设置</div>
<div flex flex-col class="link-list">
<a href="javascript:void(0)" @click="handleClickBrowserSetting">
编辑浏览器偏好<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleClickConfigLlm">
配置大语言模型
<div>
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div class="font-size-12px">
支持
<span
class="pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #3c4efd"
>DeepSeek-V3</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-black border-rd-full font-size-0.8em"
style="background-color: #fff"
>GPT-4o mini</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #462ac4"
>Qwen2.5</span
>
模型<br />支持多个服务商-模型组合按权重搭配使用
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
<TopRight w-1em h-1em mr10px />
</div>
</a>
</div>
</div>
<GlobalConfigPart />
<hr class="group-divider" />
<div class="group-item">
<div class="group-title">运行数据</div>
<div flex flex-col class="link-list">
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
<RouterLink to="./JobLibrary">职位库</RouterLink>
<RouterLink to="./BossLibrary">BOSS库</RouterLink>
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
</div>
</div>
<RunDataRecordPart />
</div>
<div class="pt-16px pb-16px flex-0 font-size-12px">
<div v-if="updateStore.availableNewRelease" mb16px>
@@ -204,12 +62,12 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
import useBuildInfo from '@renderer/hooks/useBuildInfo'
import { debounce } from 'lodash'
import { gtagRenderer } from '@renderer/utils/gtag'
import { useUpdateStore, useTaskManagerStore } from '../../store/index'
import { ElMessage } from 'element-plus'
import BossPart from './LeftNavBar/BossPart.vue'
import GlobalConfigPart from './LeftNavBar/GlabalConfigPart.vue'
import RunDataRecordPart from './LeftNavBar/RunDataRecordPart.vue'
useRouter()
@@ -223,17 +81,6 @@ const handleGotoProjectPageClick = () => {
electron.ipcRenderer.send('open-external-link', 'https://github.com/geekgeekrun/geekgeekrun')
}
const handleLaunchBossSite = debounce(
async () => {
gtagRenderer('launch_boss_site_clicked')
return await electron.ipcRenderer.invoke('open-site-with-boss-cookie', {
url: `https://www.zhipin.com/`
})
},
1000,
{ leading: true, trailing: false }
)
const updateStore = useUpdateStore()
function handleDownloadNewReleaseClick() {
gtagRenderer('click_download_release_form_nav')
@@ -246,41 +93,6 @@ function handleViewNewReleaseClick() {
const taskManagerStore = useTaskManagerStore()
void taskManagerStore
const handleClickLaunchBossLogin = async () => {
gtagRenderer('launch_login_clicked')
try {
await electron.ipcRenderer.invoke('login-with-cookie-assistant')
ElMessage({
type: 'success',
message: '登录凭据保存成功'
})
} catch {
//
}
}
const handleClickBrowserSetting = async () => {
gtagRenderer('browser_setting_clicked')
try {
await electron.ipcRenderer.invoke('config-with-browser-assistant')
ElMessage({
type: 'success',
message: '浏览器偏好保存成功'
})
} catch {
//
}
}
const handleClickConfigLlm = async () => {
gtagRenderer('config_llm_clicked')
try {
await electron.ipcRenderer.invoke('llm-config')
} catch (err) {
console.log(err)
}
}
</script>
<style lang="scss" scoped>
@@ -296,32 +108,6 @@ const handleClickConfigLlm = async () => {
margin-bottom: 4px;
margin-right: 0;
}
.group-item {
.group-title {
color: #849492;
font-size: 12px;
padding: 0.25em 0;
}
.link-list {
a {
display: flex;
align-items: center;
justify-content: space-between;
height: 2em;
box-sizing: border-box;
padding-left: 1em;
font-size: 14px;
&.router-link-active {
background-color: #fff;
font-weight: 700;
color: #2faa9e;
border-radius: 9999px 0 0 9999px;
position: relative;
box-shadow: 0px 0px 10px rgba(50, 114, 108, 0.187);
}
}
}
}
}
.feedback-button-area,
.update-button-area {

View File

@@ -22,39 +22,65 @@
<div class="pb20px"></div>
<div v-for="(item, index) in messageList" :key="index" flex flex-col flex-items-end>
<div class="message-item-wrap flex flex-col">
<div
class="message-item"
:class="{
'will-enter-context': getIsEnterContent(index)
}"
>
{{ item.text }}
</div>
<div
:style="{
width: 'fit-content',
alignSelf: 'flex-end'
}"
font-size-10px
>
{{ item.usedLlmConfig.model }}
</div>
<div
v-if="item?.usedLlmConfig?.providerCompleteApiUrl?.trim()"
:style="{
width: 'fit-content',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
alignSelf: 'flex-end',
color: '#bbb'
}"
font-size-10px
w-fit-content
max-w-20em
>
{{ item.usedLlmConfig.providerCompleteApiUrl }}
</div>
<template v-if="item.type === 'text'">
<div
class="message-item"
:class="{
'will-enter-context': getIsEnterContent(index)
}"
>
{{ item.text }}
</div>
</template>
<template v-else>
<div
class="message-item image-message-item"
:class="{
'will-enter-context': getIsEnterContent(index)
}"
>
<img :src="item.imageUrl" alt="" />
</div>
</template>
<!-- eslint-disable-next-line prettier/prettier -->
<template v-if="(typeof item.usedLlmConfig !== 'string')">
<div
:style="{
width: 'fit-content',
alignSelf: 'flex-end'
}"
font-size-10px
>
{{ item.usedLlmConfig.model }}
</div>
<div
v-if="item?.usedLlmConfig?.providerCompleteApiUrl?.trim()"
:style="{
width: 'fit-content',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
alignSelf: 'flex-end',
color: '#bbb'
}"
font-size-10px
w-fit-content
max-w-20em
>
{{ item.usedLlmConfig.providerCompleteApiUrl }}
</div>
</template>
<template v-else>
<div
:style="{
width: 'fit-content',
alignSelf: 'flex-end'
}"
font-size-10px
>
{{ item.usedLlmConfig }}
</div>
</template>
</div>
</div>
<div class="pb20px"></div>
@@ -147,15 +173,27 @@ import { computed, ref, watch } from 'vue'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { ElMessage } from 'element-plus'
import { gtagRenderer } from '@renderer/utils/gtag'
import {
OPEN_CONTENT_SOURCE,
RECHAT_CONTENT_SOURCE
} from '../../../../common/enums/auto-start-chat'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../../common/constant'
import lookForwardReplyEmotion from '../MainLayout/resources/look-forward-reply-emotion.gif'
type MessageItem = {
text: string
usedLlmConfig: string
type: 'text'
// recordInfo: any
}
const messageList = ref<MessageItem[]>([])
type ImageMessageItem = MessageItem & {
type: 'image'
imageUrl: string
}
const messageList = ref<(MessageItem | ImageMessageItem)[]>([])
const searchParams = Object.fromEntries(new URL(location.href).searchParams)
const recentMessageQuantityForLlm =
Number(new URL(location.href).searchParams.get('recentMessageQuantityForLlm')) || 8
const recentMessageQuantityForLlm = Number(searchParams.recentMessageQuantityForLlm) || 8
function getIsEnterContent(index) {
return messageList.value.length - index - 1 < recentMessageQuantityForLlm
}
@@ -188,32 +226,93 @@ watch(
const scrollElRef = ref(null)
const isLoading = ref(false)
const openContentSource = Number(searchParams.openContentSource)
const constantOpenContent = (() => {
if (searchParams.constantOpenContent?.trim()) {
return searchParams.constantOpenContent.trim()
}
if (Number(searchParams.rechatContentSource) === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join(``)
} else {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
}
})()
const rechatContentSource = Number(searchParams.rechatContentSource)
async function sendLlmGeneratedContent() {
gtagRenderer('click_mock_chat_send')
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: JSON.parse(JSON.stringify((messageList.value ?? []).slice(-8))),
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
})
console.log(response)
messageList.value.push({
text: response.responseText,
usedLlmConfig: response.usedLlmConfig
})
await sleep(50)
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
top: scrollElRef.value?.scrollHeight,
behavior: 'smooth'
})
} catch (err) {
ElMessage.error({
dangerouslyUseHTMLString: true,
grouping: true,
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
})
} finally {
isLoading.value = false
if (!(messageList.value ?? []).length) {
// send open content
if (openContentSource === OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: [],
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
})
console.log(response)
messageList.value.push({
type: 'text',
text: response.responseText,
usedLlmConfig: response.usedLlmConfig
})
await sleep(50)
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
top: scrollElRef.value?.scrollHeight,
behavior: 'smooth'
})
} catch (err) {
ElMessage.error({
dangerouslyUseHTMLString: true,
grouping: true,
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
})
} finally {
isLoading.value = false
}
} else {
messageList.value.push({
type: 'text',
text: constantOpenContent,
usedLlmConfig: '未使用大模型'
})
}
} else {
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: JSON.parse(JSON.stringify((messageList.value ?? []).slice(-8))),
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
})
console.log(response)
messageList.value.push({
type: 'text',
text: response.responseText,
usedLlmConfig: response.usedLlmConfig
})
await sleep(50)
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
top: scrollElRef.value?.scrollHeight,
behavior: 'smooth'
})
} catch (err) {
ElMessage.error({
dangerouslyUseHTMLString: true,
grouping: true,
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
})
} finally {
isLoading.value = false
}
} else {
messageList.value.push({
type: 'image',
text: `[盼回复] 表情`,
imageUrl: lookForwardReplyEmotion,
usedLlmConfig: '未使用大模型'
})
}
}
}
@@ -264,5 +363,12 @@ gtagRenderer('enter_mock_chat_page')
}
}
}
.message-item.image-message-item {
background-color: transparent;
width: 128px;
img {
width: 100%;
}
}
}
</style>

View File

@@ -59,6 +59,13 @@ const routes: Array<RouteRecordRaw> = [
title: '已读不回自动复聊 大语言模型测试'
}
},
{
path: '/commonJobConditionConfig',
component: () => import('@renderer/page/CommonJobConditionConfig/index.vue'),
meta: {
title: '公共职位筛选条件'
}
},
{
path: '/main-layout',
component: () => import('@renderer/page/MainLayout/index.vue'),

16
packages/utils/number.mjs Normal file
View File

@@ -0,0 +1,16 @@
export function hasIntersection(interval1, interval2) {
// 通用函数将区间标准化null转换为对应的无穷大
const normalizeInterval = (interval) => {
const [start, end] = interval;
return [
[null, undefined].includes(start) ? -Infinity : start,
[null, undefined].includes(end) ? Infinity : end
];
};
const [norm1Start, norm1End] = normalizeInterval(interval1);
const [norm2Start, norm2End] = normalizeInterval(interval2);
// 判断交集
return Math.max(norm1Start, norm2Start) <= Math.min(norm1End, norm2End);
}