Merge branch 'feature/ui'

This commit is contained in:
geekgeekrun
2025-07-13 21:10:34 +08:00
26 changed files with 1600 additions and 361 deletions

View File

@@ -90,7 +90,7 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
//#region get count of combinations
// 计算符合限制条件的组合数量
export function calculateTotalCombinations(selectedFilters) {
export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) {
const {
salaryList = [],
experienceList = [],
@@ -106,8 +106,21 @@ export function calculateTotalCombinations(selectedFilters) {
const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
return [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
let result = [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
return accu * cur.length
}, 1)
if (!includeEmptyCondition) {
result -= 1
}
return result
}
//#endregion
export function checkAnyCombineBossRecommendFilterHasCondition(value) {
if (!Object.keys(value ?? {}).length) {
return false
}
return Object.keys(value).some((k) => {
return !!value[k]?.length
})
}

View File

@@ -1,3 +1,7 @@
export const activeDescList = [
'', "半年前活跃","近半年活跃","5月内活跃","4月内活跃","3月内活跃","2月内活跃","本月活跃","2周内活跃","本周活跃","3日内活跃","昨日活跃","今日活跃","刚刚活跃"
]
]
export const RECOMMEND_JOB_ENTRY_SELECTOR = `.c-expect-select a[ka="jobs_recommend_tab_click"]`
export const USER_SET_EXPECT_JOB_ENTRIES_SELECTOR = `.c-expect-select .expect-list .expect-item`
export const SEARCH_BOX_SELECTOR = `.c-search-input .search-input-box`

View File

@@ -6,6 +6,7 @@
"scaleList": [],
"industryList": []
},
"isSkipEmptyConditionForCombineRecommendJobFilter": false,
"expectJobRegExpStr": "",
"jobNotMatchStrategy": 1,
"jobNotActiveStrategy": 1,
@@ -13,6 +14,7 @@
"expectCityList": [],
"expectCityNotMatchStrategy": 3,
"strategyScopeOptionWhenMarkJobCityNotMatch": 2,
"jobDetailRegExpMatchLogic": 1,
"autoReminder": {
"throttleIntervalMinutes": 10,
"rechatLimitDay": 21,
@@ -21,5 +23,11 @@
"recentMessageQuantityForLlm": 8,
"rechatLlmFallback": 1,
"onlyRemindBossWithExpectJobType": true
}
},
"jobSourceList": [
{
"type": "expect",
"enabled": true
}
]
}

View File

@@ -12,12 +12,21 @@ import { EventEmitter } from 'node:events'
import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
import { readConfigFile, writeStorageFile, ensureConfigFileExist, readStorageFile, ensureStorageFileExist } from './runtime-file-utils.mjs'
import { calculateTotalCombinations, combineFiltersWithConstraintsGenerator } from './combineCalculator.mjs'
import {
calculateTotalCombinations,
combineFiltersWithConstraintsGenerator,
checkAnyCombineBossRecommendFilterHasCondition
} from './combineCalculator.mjs'
import { default as jobFilterConditions } from './internal-config/job-filter-conditions-20241002.json'
import { default as rawIndustryFilterExemption } from './internal-config/job-filter-industry-filter-exemption-20241002.json'
import { ChatStartupFrom } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatStartupLog'
import { MarkAsNotSuitReason, MarkAsNotSuitOp, StrategyScopeOptionWhenMarkJobNotMatch, SalaryCalculateWay } from '@geekgeekrun/sqlite-plugin/dist/enums'
import { activeDescList } from './constant.mjs'
import { MarkAsNotSuitReason, MarkAsNotSuitOp, StrategyScopeOptionWhenMarkJobNotMatch, SalaryCalculateWay, JobDetailRegExpMatchLogic, JobSource } from '@geekgeekrun/sqlite-plugin/dist/enums'
import {
activeDescList,
RECOMMEND_JOB_ENTRY_SELECTOR,
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
SEARCH_BOX_SELECTOR,
} from './constant.mjs'
import { parseSalary } from "@geekgeekrun/sqlite-plugin/dist/utils/parser"
const jobFilterConditionsMapByCode = {}
Object.values(jobFilterConditions).forEach(arr => {
@@ -78,6 +87,10 @@ const bossLocalStorage = readStorageFile('boss-local-storage.json')
const targetCompanyList = readConfigFile('target-company-list.json').filter(it => !!it.trim());
const anyCombineRecommendJobFilter = readConfigFile('boss.json').anyCombineRecommendJobFilter
let isSkipEmptyConditionForCombineRecommendJobFilter = readConfigFile('boss.json').isSkipEmptyConditionForCombineRecommendJobFilter
if (!checkAnyCombineBossRecommendFilterHasCondition(anyCombineRecommendJobFilter)) {
isSkipEmptyConditionForCombineRecommendJobFilter = false
}
const expectJobRegExpStr = readConfigFile('boss.json').expectJobRegExpStr
const jobNotMatchStrategy = readConfigFile('boss.json').jobNotMatchStrategy ?? MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
@@ -99,6 +112,8 @@ const expectWorkExpList = readConfigFile('boss.json').expectWorkExpList ?? []
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
const markAsNotActiveSelectedTimeRange = (() => {
let n = readConfigFile('boss.json').markAsNotActiveSelectedTimeRange
if (
@@ -132,6 +147,60 @@ if (
expectJobDescRegExpStr = expectJobRegExpStr
}
if (
[
expectJobNameRegExpStr,
expectJobTypeRegExpStr,
expectJobDescRegExpStr,
].map(it => Boolean(it?.trim())).every(it => !it)
) {
jobDetailRegExpMatchLogic = JobDetailRegExpMatchLogic.EVERY
}
let {
jobSourceList
} = readConfigFile('boss.json')
const normalizedJobSource = []
const addedSourceSet = new Set()
for (const source of (jobSourceList ?? [])) {
if (addedSourceSet.has(source.type)) {
continue
}
if (!source?.enabled) {
continue
}
if (source.type === 'search') {
for (const searchOption of (source.children ?? [])) {
if (!searchOption.enabled || !searchOption.keyword?.trim()) {
continue
}
const key = [
source.type,
searchOption.keyword.trim()
].join('__')
if (addedSourceSet.has(key)) {
continue
}
normalizedJobSource.push({
type: 'search',
keyword: searchOption.keyword.trim()
})
addedSourceSet.add(key)
}
addedSourceSet.add(source.type)
}
else {
normalizedJobSource.push({
type: source.type,
})
addedSourceSet.add(source.type)
}
}
if (!normalizedJobSource?.length) {
normalizedJobSource.push({
type: 'expect'
})
}
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
const recommendJobPageUrl = `https://www.zhipin.com/web/geek/jobs`
@@ -262,8 +331,8 @@ async function markJobAsNotSuitInRecommendPage (reasonCode) {
return result
}
export function testIfJobTitleOrDescriptionSuit (jobInfo) {
let isJobNameSuit = true
export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
let isJobNameSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
try {
if (expectJobNameRegExpStr.trim()) {
const regExp = new RegExp(expectJobNameRegExpStr, 'i')
@@ -271,7 +340,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) {
}
} catch {
}
let isJobTypeSuit = true
let isJobTypeSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
try {
if (expectJobTypeRegExpStr.trim()) {
const regExp = new RegExp(expectJobTypeRegExpStr, 'i')
@@ -279,7 +348,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) {
}
} catch {
}
let isJobDescSuit = true
let isJobDescSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
try {
if (expectJobDescRegExpStr.trim()) {
const regExp = new RegExp(expectJobDescRegExpStr, 'i')
@@ -287,7 +356,12 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo) {
}
} catch {
}
return isJobNameSuit && isJobTypeSuit && isJobDescSuit
if (matchLogic === JobDetailRegExpMatchLogic.SOME) {
return isJobNameSuit || isJobTypeSuit || isJobDescSuit
}
else {
return isJobNameSuit && isJobTypeSuit && isJobDescSuit
}
}
async function setFilterCondition (selectedFilters) {
@@ -465,8 +539,94 @@ async function toRecommendPage (hooks) {
}
}
const INIT_START_EXCEPT_JOB_INDEX = 0
let currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
const computedSourceList = []
for (const source of normalizedJobSource) {
switch (source.type) {
case 'recommend': {
computedSourceList.push({
type: source.type,
selector: RECOMMEND_JOB_ENTRY_SELECTOR,
async getIsCurrentActiveSource () {
return await page.evaluate(
({ RECOMMEND_JOB_ENTRY_SELECTOR }) => {
return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active')
}, {
RECOMMEND_JOB_ENTRY_SELECTOR
}
)
},
async setToActiveSource() {
// not first navigation and should choose a job (except job)
// click first expect job
const expectJobTabHandler = await page.$(RECOMMEND_JOB_ENTRY_SELECTOR)
await expectJobTabHandler.click() // switch to first condition
}
})
continue
}
case 'expect': {
await page.waitForSelector(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
const allExpectJobEntryHandles = await page.$$(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
allExpectJobEntryHandles.forEach((it, index) => {
computedSourceList.push({
type: source.type,
selector: `${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`,
async getIsCurrentActiveSource () {
return await page.evaluate(
({
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
index
}) => {
return document.querySelector(`${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`).classList.contains('active')
}, {
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
index
}
)
},
async setToActiveSource() {
// not first navigation and should choose a job (except job)
// click first expect job
const expectJobTabHandler = await page.$(`${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`)
await expectJobTabHandler.click() // switch to first condition
}
})
})
break
}
case 'search': {
computedSourceList.push({
type: source.type,
async getIsCurrentActiveSource () {
const elHandle = await page.$(`.page-jobs-main`)
const currentKeyWord = await elHandle?.evaluate((el) => {
return el?.__vue__?.formData?.query
})
if (!currentKeyWord) {
return false
}
return currentKeyWord === source.keyword
},
async setToActiveSource() {
await page.waitForSelector(SEARCH_BOX_SELECTOR)
const inputHandle = await page.$(`${SEARCH_BOX_SELECTOR} input`)
await inputHandle.focus()
await sleep(100)
let currentValue = await inputHandle.evaluate(el => el.value)
while (currentValue) {
await inputHandle.press('Backspace')
currentValue = await inputHandle.evaluate(el => el.value)
}
await inputHandle.type(source.keyword?.trim() || '', { delay: 100 })
await sleep(500)
await inputHandle.press('Enter')
}
})
}
}
}
let currentSourceIndex = 0
afterPageLoad: while (true) {
let expectJobList
iterateFilterCondition: for (
@@ -478,26 +638,44 @@ async function toRecommendPage (hooks) {
await sleepWithRandomDelay(2500)
await Promise.all([
page.waitForSelector('.c-expect-select .expect-list .expect-item'),
page.waitForSelector('.job-list-container .rec-job-list')
Promise.race([
page.waitForSelector(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR),
page.waitForSelector(RECOMMEND_JOB_ENTRY_SELECTOR),
]),
Promise.race([
page.waitForSelector(".job-list-container .rec-job-list"),
page.waitForSelector(".recommend-result-job .job-empty-wrapper")
])
])
await page.click(`.c-expect-select .expect-list .expect-item`)
const currentActiveJobIndex = await page.evaluate(`
[...document.querySelectorAll('.c-expect-select .expect-list .expect-item')].findIndex(it => it.classList.contains('active'))
`)
// await page.click(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)
await sleep(3000)
let onPageCurrentSourceIndex = -1
for (let i=0; i < computedSourceList.length; i++) {
const computedSource = computedSourceList[i]
if (await computedSource.getIsCurrentActiveSource()) {
onPageCurrentSourceIndex = i
break
}
}
if (
isSkipEmptyConditionForCombineRecommendJobFilter &&
Object.keys(filterCondition).length &&
Object.keys(filterCondition).every(k => !filterCondition[k]?.length)
) {
sleep(4000)
continue iterateFilterCondition
}
expectJobList = await page.evaluate(`document.querySelector('.c-expect-select')?.__vue__?.expectList`)
if (currentActiveJobIndex === currentExceptJobIndex) {
if (onPageCurrentSourceIndex === currentSourceIndex) {
// first navigation and can immediately start chat (recommend job)
} else {
// not first navigation and should choose a job (except job)
// click first expect job
const expectJobTabHandlers = await page.$$('.c-expect-select .expect-list .expect-item')
await expectJobTabHandlers[currentExceptJobIndex].click()
await computedSourceList[currentSourceIndex].setToActiveSource()
await page.waitForResponse(
response => {
if (
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json')
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json') ||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/search/joblist.json')
) {
return true
}
@@ -507,9 +685,12 @@ async function toRecommendPage (hooks) {
await storeStorage(page).catch(() => void 0)
await sleepWithRandomDelay(2000)
}
await sleepWithRandomDelay(3000)
await sleepWithRandomDelay(1500)
await setFilterCondition(filterCondition)
await sleep(1500) // TODO: accurately check if job list request sent and response received after set condition
await page.waitForFunction(() => {
return !document.querySelector('.job-recommend-result .job-rec-loading')
})
try {
const { targetJobIndex, targetJobData } = await new Promise(async (resolve, reject) => {
try {
@@ -517,7 +698,10 @@ async function toRecommendPage (hooks) {
page.on(
'request',
function reqHandler (request) {
if (request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json')) {
if (
request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/pc/recommend/job/list.json') ||
request.url().startsWith('https://www.zhipin.com/wapi/zpgeek/search/joblist.json')
) {
requestNextPagePromiseWithResolver = (() => {
const o = {}
o.promise = new Promise((resolve, reject) => {
@@ -540,9 +724,17 @@ async function toRecommendPage (hooks) {
}
}
)
// job list
const recommendJobListElProxy = await page.$('.job-list-container .rec-job-list')
let recommendJobListElProxy
try {
recommendJobListElProxy= await page.waitForSelector('.job-list-container .rec-job-list', { timeout: 5 * 1000 })
} catch {}
if (!recommendJobListElProxy){
await hooks.encounterEmptyRecommendJobList?.promise({
pageQuery: await page.evaluate(() => new URL(location.href).searchParams.toString())
})
throw new Error('CANNOT_FIND_EXCEPT_JOB_IN_THIS_FILTER_CONDITION')
}
let jobListData = []
async function updateJobListData () {
jobListData = await page.evaluate(`document.querySelector('.page-jobs-main')?.__vue__?.jobList`)
@@ -739,7 +931,22 @@ async function toRecommendPage (hooks) {
const notSuitConditionHandleMap = {
async active() {
blockBossNotActive.add(targetJobData.jobInfo.encryptUserId)
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
}
}
else if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
try {
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.BOSS_INACTIVE)
await hooks.jobMarkedAsNotSuit.promise(
@@ -751,21 +958,8 @@ async function toRecommendPage (hooks) {
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
chosenReasonInUi
},
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
}
)
} catch {
}
}
else if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.BOSS_INACTIVE,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
@@ -774,7 +968,22 @@ async function toRecommendPage (hooks) {
},
async city() {
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.JOB_CITY_NOT_SUIT,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
}
}
else if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
try {
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
await hooks.jobMarkedAsNotSuit.promise(
@@ -785,21 +994,8 @@ async function toRecommendPage (hooks) {
extInfo: {
chosenReasonInUi
},
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
}
)
} catch {
}
}
else if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.JOB_CITY_NOT_SUIT,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
@@ -808,7 +1004,22 @@ async function toRecommendPage (hooks) {
},
async workExp() {
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
}
}
else if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
try {
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT)
await hooks.jobMarkedAsNotSuit.promise(
@@ -819,21 +1030,8 @@ async function toRecommendPage (hooks) {
extInfo: {
chosenReasonInUi
},
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
}
)
} catch {
}
}
else if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
@@ -842,7 +1040,22 @@ async function toRecommendPage (hooks) {
},
async jobDetail() {
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
}
}
else if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
try {
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_NOT_SUIT)
await hooks.jobMarkedAsNotSuit.promise(
@@ -854,21 +1067,8 @@ async function toRecommendPage (hooks) {
bossActiveTimeDesc: targetJobData.bossInfo.activeTimeDesc,
chosenReasonInUi
},
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
}
)
} catch {
}
}
else if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.JOB_NOT_SUIT,
extInfo: null,
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
@@ -877,7 +1077,24 @@ async function toRecommendPage (hooks) {
},
async salary() {
blockJobNotSuit.add(targetJobData.jobInfo.encryptId)
if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL || !await page.$('.job-detail-box .job-detail-operate .not-suitable')) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT,
extInfo: {
salaryDesc: selectedJobData.salaryDesc,
},
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
}
}
else if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
try {
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT)
await hooks.jobMarkedAsNotSuit.promise(
@@ -889,23 +1106,8 @@ async function toRecommendPage (hooks) {
salaryDesc: selectedJobData.salaryDesc,
chosenReasonInUi
},
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
}
)
} catch {
}
}
else if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
try {
await hooks.jobMarkedAsNotSuit.promise(
targetJobData,
{
markFrom: ChatStartupFrom.AutoFromRecommendList,
markReason: MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT,
extInfo: {
salaryDesc: selectedJobData.salaryDesc,
},
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
markOp: MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
} catch {
@@ -938,7 +1140,7 @@ async function toRecommendPage (hooks) {
notSuitReasonIdToStrategyMap.workExp = expectWorkExpNotMatchStrategy
}
if (
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo)
!testIfJobTitleOrDescriptionSuit(targetJobData.jobInfo, jobDetailRegExpMatchLogic)
) {
notSuitReasonIdToStrategyMap.jobDetail = jobNotMatchStrategy
}
@@ -960,7 +1162,7 @@ async function toRecommendPage (hooks) {
// 2. if there is no condition to mark Boss, then find the one mark on local db
const markOnLocalDbCondition = Object.keys(notSuitReasonIdToStrategyMap).find(k => notSuitReasonIdToStrategyMap[k] === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL)
if (markOnLocalDbCondition) {
await notSuitConditionHandleMap[markOnBossCondition]()
await notSuitConditionHandleMap[markOnLocalDbCondition]()
continue continueFind
}
// #endregion
@@ -1035,7 +1237,13 @@ async function toRecommendPage (hooks) {
throw new Error('STARTUP_CHAT_ERROR_WITH_UNKNOWN_ERROR')
}
} else {
await hooks.newChatStartup?.promise(targetJobData, { chatStartupFrom: ChatStartupFrom.AutoFromRecommendList })
await hooks.newChatStartup?.promise(
targetJobData,
{
chatStartupFrom: ChatStartupFrom.AutoFromRecommendList,
jobSource: JobSource[computedSourceList[currentSourceIndex]?.type]
}
)
blockBossNotNewChat.add(targetJobData.jobInfo.encryptUserId)
await storeStorage(page).catch(() => void 0)
@@ -1078,20 +1286,20 @@ async function toRecommendPage (hooks) {
}
// for of reach terminal
if (
currentExceptJobIndex + 1 >= expectJobList.length
currentSourceIndex + 1 >= computedSourceList.length
) {
hooks.noPositionFoundForCurrentJob?.call()
hooks.noPositionFoundAfterTraverseAllJob?.call()
await sleep((20 + 30 * Math.random()) * 1000)
await Promise.all([
page.reload(),
page.goto(`https://www.zhipin.com/web/geek/jobs`),
page.waitForNavigation()
])
currentExceptJobIndex = INIT_START_EXCEPT_JOB_INDEX
currentSourceIndex = 0
} else {
hooks.noPositionFoundForCurrentJob?.call()
await sleep((10 + 15 * Math.random()) * 1000)
currentExceptJobIndex += 1
currentSourceIndex += 1
}
}
}
@@ -1118,12 +1326,13 @@ export async function mainLoop (hooks) {
}
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
await page.bringToFront()
await hooks.mainFlowWillLaunch?.callAsync({
await hooks.mainFlowWillLaunch?.promise({
jobNotMatchStrategy,
jobNotActiveStrategy,
expectCityNotMatchStrategy,
blockJobNotSuit,
blockBossNotActive,
blockBossNotNewChat
})
await toRecommendPage(hooks)
// goto search

View File

@@ -56,7 +56,8 @@ const main = async () => {
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
noPositionFoundForCurrentJob: new SyncHook(),
noPositionFoundAfterTraverseAllJob: new SyncHook(),
errorEncounter: new SyncHook(['errorInfo'])
errorEncounter: new SyncHook(['errorInfo']),
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args'])
}
initPlugins(hooks)
await hooks.daemonInitialized.callAsync()

View File

@@ -1,3 +1,4 @@
import { JobSource } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
@@ -29,4 +30,9 @@ export class ChatStartupLog {
nullable: true
})
autoStartupChatRecordId?: number;
@Column({
nullable: true
})
jobSource?: JobSource;
}

View File

@@ -1,4 +1,4 @@
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
import { ChatStartupFrom } from "./ChatStartupLog";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
@@ -41,4 +41,9 @@ export class MarkAsNotSuitLog {
nullable: true
})
autoStartupChatRecordId?: number;
@Column({
nullable: true
})
jobSource?: JobSource;
}

View File

@@ -22,4 +22,15 @@ export enum StrategyScopeOptionWhenMarkJobNotMatch {
export enum SalaryCalculateWay {
MONTH_SALARY = 1,
ANNUAL_PACKAGE = 2,
}
}
export enum JobDetailRegExpMatchLogic {
EVERY = 1,
SOME = 2,
}
export enum JobSource {
expect = 1,
recommend = 2,
search = 3,
}

View File

@@ -245,7 +245,7 @@ export async function saveChatStartupRecord(
ds: DataSource,
_jobInfo,
{ encryptUserId },
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined } = {}
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined, jobSource = undefined } = {}
) {
const { jobInfo } = _jobInfo;
@@ -256,7 +256,8 @@ export async function saveChatStartupRecord(
encryptCurrentUserId: encryptUserId,
encryptJobId: jobInfo.encryptId,
autoStartupChatRecordId,
chatStartupFrom
chatStartupFrom,
jobSource,
}
Object.assign(chatStartupLog, chatStartupLogPayload)
@@ -270,7 +271,7 @@ export async function saveMarkAsNotSuitRecord(
ds: DataSource,
_jobInfo,
{ encryptUserId },
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined } = {}
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined, jobSource = undefined } = {}
) {
const { jobInfo } = _jobInfo;
@@ -284,7 +285,8 @@ export async function saveMarkAsNotSuitRecord(
markFrom,
markReason,
extInfo: extInfo ? JSON.stringify(extInfo) : undefined,
markOp
markOp,
jobSource,
}
Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload)
@@ -333,9 +335,33 @@ export async function getNotSuitMarkRecordsInLastSomeDays (ds: DataSource, days
const result = await repo.findBy({
date: Raw(alias => `DATE(${alias}) >= DATE('${
new Date(
Number(new Date()) - 7 * 24 * 60 * 60 * 1000
Number(new Date()) - days * 24 * 60 * 60 * 1000
).toISOString()
}')`)
})
return result
}
export async function getChatStartupRecordsInLastSomeDays (ds: DataSource, days = 0) {
const repo = ds.getRepository(ChatStartupLog)
const result = await repo.findBy({
date: Raw(alias => `DATE(${alias}) >= DATE('${
new Date(
Number(new Date()) - days * 24 * 60 * 60 * 1000
).toISOString()
}')`)
})
return result
}
export async function getBossIdsByJobIds (ds: DataSource, jobIds: string[] = []) {
const repo = ds.getRepository(JobInfo)
const result = await repo.find({
where: jobIds.map(
id => ({
encryptJobId: id
})
)
})
return result
}

View File

@@ -23,13 +23,21 @@ import { ChatMessageRecord } from './entity/ChatMessageRecord'
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
import sqlite3 from 'sqlite3';
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord, getNotSuitMarkRecordsInLastSomeDays } from "./handlers";
import {
saveChatStartupRecord,
saveJobInfoFromRecommendPage,
saveMarkAsNotSuitRecord,
getNotSuitMarkRecordsInLastSomeDays,
getChatStartupRecordsInLastSomeDays,
getBossIdsByJobIds
} from "./handlers";
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
import minimist from 'minimist'
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
import { Init1000000000000 } from "./migrations/1000000000000-Init";
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
export function initDb(dbFilePath) {
const { DataSource } = requireTypeorm()
@@ -65,6 +73,7 @@ export function initDb(dbFilePath) {
UpdateChatStartupLogTable1729182577167,
UpdateBossInfoTable1732032381304,
AddColumnForMarkAsNotSuitLog1746092370665,
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526
],
migrationsRun: true
});
@@ -111,6 +120,7 @@ export default class SqlitePlugin {
expectCityNotMatchStrategy,
blockJobNotSuit,
blockBossNotActive,
blockBossNotNewChat
}) => {
if (
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
@@ -118,8 +128,11 @@ export default class SqlitePlugin {
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
) {
const ds = await this.initPromise;
const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7) ?? []);
if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7)) ?? [];
if (
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it =>
[
@@ -134,7 +147,10 @@ export default class SqlitePlugin {
id => blockJobNotSuit.add(id)
)
}
if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
if (
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it => it.markReason === MarkAsNotSuitReason.BOSS_INACTIVE)
.map(
@@ -144,7 +160,10 @@ export default class SqlitePlugin {
id => blockJobNotSuit.add(id)
)
}
if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL) {
if (
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it => it.markReason === MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
.map(
@@ -154,6 +173,12 @@ export default class SqlitePlugin {
id => blockJobNotSuit.add(id)
)
}
const last30DayChatStartupRecords = (await getChatStartupRecordsInLastSomeDays(ds, 30)) ?? [];
const chattedJobIds = last30DayChatStartupRecords.map(it => it.encryptJobId)
const chattedBossIds = ((await getBossIdsByJobIds(ds, chattedJobIds)) ?? []).map(it => it.encryptBossId)
for (const id of chattedBossIds) {
blockBossNotNewChat.add(id)
}
}
}
);
@@ -163,22 +188,24 @@ export default class SqlitePlugin {
await saveJobInfoFromRecommendPage(ds, _jobInfo);
});
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList } = {}) => {
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList, jobSource = undefined } = {}) => {
const ds = await this.initPromise;
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId,
chatStartupFrom
chatStartupFrom,
jobSource
});
});
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined } = {}) => {
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined, jobSource = undefined } = {}) => {
const ds = await this.initPromise;
return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId,
markFrom,
markReason,
extInfo,
markOp
markOp,
jobSource
});
});
}

View File

@@ -0,0 +1,64 @@
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm"
import { VBossLibrary } from "../entity/VBossLibrary";
import { VChatStartupLog } from "../entity/VChatStartupLog";
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
import { VJobLibrary } from "../entity/VJobLibrary";
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
import { JobSource } from "../enums";
const ViewEntities = [
VBossLibrary,
VChatStartupLog,
VCompanyLibrary,
VJobLibrary,
VMarkAsNotSuitLog,
]
export class AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
}
if (await queryRunner.hasTable("mark_as_not_suit_log")) {
if (!await queryRunner.hasColumn("mark_as_not_suit_log", "jobSource")) {
await queryRunner.addColumn(
"mark_as_not_suit_log",
new TableColumn({
name: "jobSource",
type: "number",
isNullable: true,
})
);
await queryRunner.query(`UPDATE mark_as_not_suit_log SET jobSource=?`, [JobSource.expect]);
}
}
if (await queryRunner.hasTable("chat_startup_log")) {
if (!await queryRunner.hasColumn("chat_startup_log", "jobSource")) {
await queryRunner.addColumn(
"chat_startup_log",
new TableColumn({
name: "jobSource",
type: "number",
isNullable: true,
})
);
await queryRunner.query(`UPDATE chat_startup_log SET jobSource=?`, [JobSource.expect]);
}
}
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
let expression = viewMetadata.expression;
if (typeof expression === 'function') {
expression = expression(dataSource).getQuery();
}
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View File

@@ -44,7 +44,8 @@
"pinia": "^3.0.2",
"puppeteer": "20.1.0",
"puppeteer-extra-plugin-stealth": "2.11.2",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",

View File

@@ -93,7 +93,8 @@ const runAutoChat = async () => {
jobMarkedAsNotSuit: new AsyncSeriesHook(['positionInfoDetail', 'markDetail']),
noPositionFoundForCurrentJob: new SyncHook(),
noPositionFoundAfterTraverseAllJob: new SyncHook(),
errorEncounter: new SyncHook(['errorInfo'])
errorEncounter: new SyncHook(['errorInfo']),
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args'])
}
initPlugins(hooks)

View File

@@ -4,6 +4,10 @@ import {
readStorageFile,
writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import {
RECOMMEND_JOB_ENTRY_SELECTOR,
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR,
} from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs'
import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
import {
saveJobInfoFromRecommendPage,
@@ -13,7 +17,7 @@ import {
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
import { initDb } from '@geekgeekrun/sqlite-plugin'
import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
import { MarkAsNotSuitReason, JobSource } from '@geekgeekrun/sqlite-plugin/dist/enums'
import fs from 'node:fs'
import { Target } from 'puppeteer'
@@ -38,6 +42,49 @@ const attachRequestsListener = async (target: Target) => {
if (!page) {
return
}
async function getCurrentJobSource() {
const methodMap = {
async recommend() {
return await page.evaluate(
({ RECOMMEND_JOB_ENTRY_SELECTOR }) => {
return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active')
},
{
RECOMMEND_JOB_ENTRY_SELECTOR
}
)
},
async expect() {
return await page.evaluate(
({ USER_SET_EXPECT_JOB_ENTRIES_SELECTOR }) => {
return [...document.querySelectorAll(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)].some((el) =>
el.classList.contains('active')
)
},
{
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR
}
)
},
async search() {
const elHandle = await page.$(`.page-jobs-main`)
const currentKeyWord = await elHandle?.evaluate((el) => {
return el?.__vue__?.formData?.query
})
return !!currentKeyWord
}
}
for (const [type, func] of Object.entries(methodMap)) {
try {
if (await func()) {
return type
}
} catch (err) {
console.error('encounter error when get job source')
}
}
return null
}
page.on('response', async (response) => {
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
@@ -81,7 +128,7 @@ const attachRequestsListener = async (target: Target) => {
const reasonCodeToTextMap = await readStorageFile(
'job-not-suit-reason-code-to-text-cache.json'
)
const jobSource = await getCurrentJobSource()
const markDetail = {
markFrom: ChatStartupFrom.ManuallyFromRecommendList,
extInfo: {
@@ -90,12 +137,14 @@ const attachRequestsListener = async (target: Target) => {
text: reasonCodeToTextMap[chosenCode]
}
},
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON,
jobSource: JobSource[jobSource]
}
gtag('job_marked_as_not_suit', {
markFrom: markDetail.markFrom,
bossActiveTimeDesc: currentJobData?.bossInfo?.activeTimeDesc,
encryptJobId: currentJobData?.jobInfo?.encryptId
encryptJobId: currentJobData?.jobInfo?.encryptId,
jobSource: JobSource[jobSource]
})
if (reasonCodeToTextMap[chosenCode]?.includes('活跃度低')) {
markDetail.markReason = MarkAsNotSuitReason.BOSS_INACTIVE
@@ -131,9 +180,11 @@ const attachRequestsListener = async (target: Target) => {
const currentUserInfo = await page.evaluate(
'document.querySelector(".job-detail-box").__vue__.$store.state.userInfo'
)
const jobSource = await getCurrentJobSource()
gtag('new_chat_startup', {
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
encryptJobId: currentJobData?.jobInfo?.encryptId
encryptJobId: currentJobData?.jobInfo?.encryptId,
jobSource: JobSource[jobSource]
})
await saveChatStartupRecord(
await dbInitPromise,
@@ -142,7 +193,8 @@ const attachRequestsListener = async (target: Target) => {
encryptUserId: currentUserInfo.encryptUserId
},
{
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
jobSource: JobSource[jobSource]
}
)
} else if (

View File

@@ -149,6 +149,16 @@ export default function initIpc() {
bossConfig.strategyScopeOptionWhenMarkJobWorkExpNotMatch =
payload.strategyScopeOptionWhenMarkJobWorkExpNotMatch
}
if (hasOwn(payload, 'jobDetailRegExpMatchLogic')) {
bossConfig.jobDetailRegExpMatchLogic = payload.jobDetailRegExpMatchLogic
}
if (hasOwn(payload, 'isSkipEmptyConditionForCombineRecommendJobFilter')) {
bossConfig.isSkipEmptyConditionForCombineRecommendJobFilter =
payload.isSkipEmptyConditionForCombineRecommendJobFilter
}
if (hasOwn(payload, 'jobSourceList')) {
bossConfig.jobSourceList = payload.jobSourceList
}
promiseArr.push(writeConfigFile('boss.json', bossConfig))

View File

@@ -2,26 +2,34 @@ import gtag from '.'
export default class GtagPlugin {
apply(hooks) {
hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom }) => {
hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom, jobSource }) => {
gtag('new_chat_startup', {
chatStartupFrom,
encryptJobId: jobData.jobInfo.encryptId
})
})
hooks.jobMarkedAsNotSuit.tap('GtagPlugin', (jobData, { markFrom, markOp, markReason }) => {
gtag('job_marked_as_not_suit', {
markFrom,
markOp,
markReason,
bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc,
encryptJobId: jobData.jobInfo.encryptId
encryptJobId: jobData.jobInfo.encryptId,
jobSource
})
})
hooks.jobMarkedAsNotSuit.tap(
'GtagPlugin',
(jobData, { markFrom, markOp, markReason, jobSource }) => {
gtag('job_marked_as_not_suit', {
markFrom,
markOp,
markReason,
bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc,
encryptJobId: jobData.jobInfo.encryptId,
jobSource
})
}
)
hooks.noPositionFoundForCurrentJob.tap('GtagPlugin', () => {
gtag('no_position_found_for_current_job')
})
hooks.noPositionFoundAfterTraverseAllJob.tap('GtagPlugin', () => {
gtag('no_position_found_after_traverse_all_job')
})
hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => {
gtag('encounter_empty_rec_job_list', { pageQuery })
})
}
}

View File

@@ -0,0 +1,357 @@
<template>
<draggable
:model-value="modelValue"
class="list-group"
:component-data="{
tag: 'ul',
type: 'transition-group',
name: !drag ? 'flip-list' : null
}"
:group="{ name: 'parent', put: ['parent'] }"
v-bind="dragOptions"
handle=".drag-handle-wrap"
@update:model-value="emit('update:model-value', $event)"
@start="drag = true"
@end="
() => {
drag = false
setTimeout(() => {
gtagRenderer('job-source-dragged', {
sourceOrder: modelValue?.map((it) => JobSource[it.type]).join(','),
enabledSourceOrder: modelValue
?.filter?.((it) => !!it.enabled)
?.map((it) => JobSource[it.type])
?.join(',')
})
}, 50)
}
"
>
<template #item="{ element }">
<li class="list-group-item">
<div flex flex-items-center>
<span class="drag-handle-wrap">
<span class="drag-handle" />
</span>
<span class="inner-content">
<div flex w-full flex-1>
<template
v-if="
element.type === 'search' &&
(!element?.children?.length ||
element?.children?.every((it) => !(it.enabled && !!it.keyword?.trim())))
"
>
<el-switch
model-value="false"
mr10px
disabled
active-text="启用"
inactive-text="禁用"
inline-prompt
@click="
() => {
if (!element?.children?.length) {
Message.info({
message: '请添加一个要搜索的关键词',
grouping: true
})
} else if (
element?.children?.every((it) => !(it.enabled && !!it.keyword?.trim()))
) {
Message.info({
message: '请启用一个要搜索的关键词',
grouping: true
})
}
gtagRenderer('job-source-switch-ph-clicked', { type: element.type })
}
"
/>
{{ element.label }}
</template>
<template v-else>
<el-switch
v-model="element.enabled"
mr10px
active-text="启用"
inactive-text="禁用"
inline-prompt
@change="
(v) => gtagRenderer('job-source-switch-changed', { type: element.type, v })
"
/>
{{ element.label }}
</template>
</div>
<div v-if="element.type === 'search'">
<div flex flex-items-center>
<span color-orange align-self-end mr10px>
{{ getSearchSourceTipText(element) }}
</span>
<el-button
p-0
h-fit
type="text"
@click="
() => {
addSearchKeyword(element)
gtagRenderer('job-source-search-kw-added', {
kwListLength: element.children?.length
})
}
"
>添加关键词</el-button
>
</div>
</div>
</span>
</div>
<div v-if="element.type === 'search' && element?.children?.length">
<draggable
v-model="element.children"
class="list-group"
:component-data="{
tag: 'ul',
type: 'transition-group',
name: !drag ? 'flip-list' : null
}"
:group="{ name: 'child', put: ['child'] }"
v-bind="dragOptions"
handle=".drag-handle-wrap"
@start="drag = true"
@end="
() => {
drag = false
setTimeout(() => {
gtagRenderer('job-source-search-kw-dragged', {
kwListLength: element.children?.length
})
}, 50)
}
"
>
<template #item="{ element: searchItem, index }">
<li class="list-group-item">
<div flex flex-items-center>
<span class="drag-handle-wrap">
<span class="drag-handle" />
</span>
<span class="inner-content">
<div flex w-full>
<el-switch
v-if="element.enabled && searchItem.keyword?.trim()"
v-model="searchItem.enabled"
mr10px
active-text="启用"
inactive-text="禁用"
inline-prompt
@change="(v) => gtagRenderer('job-source-search-kw-switch-changed', { v })"
/>
<el-switch
v-else
disabled
:model-value="false"
mr10px
active-text="启用"
inactive-text="禁用"
inline-prompt
@click="
() => {
if (!searchItem.keyword?.trim()) {
Message.info({
message: '该条关键词为空,请输入要搜索的关键词',
grouping: true
})
} else if (!element.enabled) {
Message.info({
message: '请启用“通过搜索找到的职位”来源',
grouping: true
})
}
gtagRenderer('job-source-search-kw-switch-ph-clicked')
}
"
/>
<el-input
v-model="searchItem.keyword"
maxlength="100"
@blur="
() => {
searchItem.keyword = searchItem.keyword?.trim() ?? ''
gtagRenderer('job-source-search-kw-input-blurred', {
contentLength: searchItem.keyword?.length
})
}
"
/>
</div>
<el-button
p-0
ml-10px
h-fit
type="danger"
link
@click="
() => {
removeSearchKeywordByIndex(element, index)
gtagRenderer('job-source-search-kw-removed', {
itemIndex: index,
contentLength: searchItem.keyword?.length
})
}
"
>删除</el-button
>
</span>
</div>
</li>
</template>
</draggable>
</div>
</li>
</template>
</draggable>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import draggable from 'vuedraggable'
import { ElMessage as Message } from 'element-plus'
import { gtagRenderer } from '@renderer/utils/gtag'
import { JobSource } from '@geekgeekrun/sqlite-plugin/src/enums'
const props = defineProps({
modelValue: {
type: Array
}
})
const setTimeout = globalThis.setTimeout.bind(globalThis)
const emit = defineEmits(['update:model-value'])
const drag = ref(false)
const dragOptions = computed(() => {
return {
animation: 200,
disabled: false,
ghostClass: 'ghost'
}
})
function addSearchKeyword(item) {
if (!item.children) {
item.children = []
}
item.enabled = true
item.children.push({
type: 'search-kw',
enabled: true,
keyword: ''
})
}
function removeSearchKeywordByIndex(item, index) {
item.children?.splice(index, 1)
}
const searchKeywordCountMap = computed(() => {
const target = props.modelValue?.find((it) => it.type === 'search' && it.enabled)
if (!target || !target?.children?.length) {
return {}
}
const map = {}
target.children?.forEach((it) => {
if (!it.keyword) {
return
}
if (!map[it.keyword]) {
map[it.keyword] = 0
}
map[it.keyword]++
})
return map
})
const getSearchSourceTipText = (element) => {
if (!element?.children?.length) {
return `添加一个关键词后方可启用->`
}
const seg: string[] = []
if (element.enabled) {
if (element.children?.every((it) => !(it.enabled && !!it.keyword?.trim()))) {
seg.push(`启用下方任一非空项后方可启用`)
} else if (element.children?.some((it) => it.enabled && !it.keyword?.trim())) {
seg.push(`空项会被跳过`)
}
if (Object.values(searchKeywordCountMap.value).some((n) => n > 1)) {
seg.push(`重复项仅取第一个启用的项`)
}
}
return seg.join('')
}
</script>
<style lang="scss" scoped>
.ghost {
opacity: 0.5;
filter: blur(2px);
}
.list-group {
line-height: 32px;
min-height: 20px;
.list-group {
margin-left: 20px;
}
.list-group-item {
background-color: #fff;
list-style: none;
display: flex;
flex-direction: column;
padding-top: 6px;
padding-bottom: 6px;
padding-right: 10px;
border: 1px solid var(--el-card-border-color);
margin-top: 6px;
margin-bottom: 6px;
box-shadow: var(--el-box-shadow-light);
border-radius: 4px;
.inner-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
::v-deep(.el-checkbox) {
.el-checkbox__label {
width: 100%;
}
}
}
.drag-handle-wrap {
display: flex;
justify-content: flex-end;
width: 25px;
height: 1em;
cursor: grab;
margin-right: 4px;
.drag-handle {
width: 20px;
background-image: linear-gradient(
90deg,
transparent 35%,
var(--el-card-border-color) 35%,
var(--el-card-border-color) 45%,
transparent 45%,
transparent 55%,
var(--el-card-border-color) 55%,
var(--el-card-border-color) 65%,
transparent 65%,
transparent 100%
);
}
}
}
}
</style>

View File

@@ -109,8 +109,14 @@ import { ElForm, ElMessage } from 'element-plus'
import { ref, onUnmounted, onMounted } from 'vue'
import { checkCookieListFormat } from '../../../../common/utils/cookie'
import { useRouter } from 'vue-router'
import { gtagRenderer } from '@renderer/utils/gtag'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'cookie-assistant',
...params
})
}
const router = useRouter()
const cookieInvalid = ref(false)

View File

@@ -1,5 +1,5 @@
<template>
<div class="first-run-readme" ref="componentRootEl">
<div ref="componentRootEl" class="first-run-readme">
<div class="first-run-readme__inner">
<div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div>
<div class="readme-desc">
@@ -12,25 +12,32 @@
>
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
本程序从某种程度上说属于辅助工具<el-link
@click="
electron.ipcRenderer.send(
'open-external-link',
'https://about.zhipin.com/agreement/'
)
color-blue
@click.stop.prevent="
() => {
electron.ipcRenderer.send(
'open-external-link',
'https://about.zhipin.com/agreement/'
)
gtagRenderer('view_boss_agreement_clicked')
}
"
>Boss直聘用户协议</el-link
>相关条款相违背
根据该条款如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b
class="color-red"
>账号被强制退出登录</b
><b class="color-red">账号被限制使用</b><b class="color-red">账号被封禁</b
>2023年3月版相关条款相违背您在注册Boss直聘时已签署过这一条款根据该条款
<i>用户的平台使用义务</i><i>违约责任</i>
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录账号被限制使用账号被封禁</b
>等对您不利的风险因此使用本程序即意味着<b class="color-red">您愿意接受以上风险</b
>且如果相关风险发生<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
@@ -38,39 +45,58 @@
>注册一个本程序专用的新的Boss直聘账号</b
>进行求职
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序原理是模拟用户在Boss直聘网页上进行点击操作Boss直聘网站每过一段时间会发生改版且有可能包含A/B实验这将导致本程序相关脚本失效此时您可以点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
您的雇主可能会对您的计算机终端或网络进行监控从而审计跟踪您的行为建议您<b
本程序原理是模拟用户在Boss直聘网页上寻找关键元素并进行点击操作Boss直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red"
>不要在您雇主提供的计算机终端或网络上使用本程序</b
>
>浏览器重复闪退重新启动</b
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊Boss以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
您所在公司可能会对您的计算机终端或网络进行<b color-red>监控</b>从而<b color-red
>审计跟踪</b
>您的行为上级 / HRBP 团队可能会从 IT
团队处获取到监控数据从而了解团队成员离职倾向如果您不希望上级 / HRBP
团队了解到您正在求职建议您<b color-red
>不要在您所在公司提供的计算机终端或网络上使用本程序</b
>
</ElCheckbox>
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
本程序<b class="color-red"
>不会上报您的隐私诸如能够识别出您身份的信息不会向您的雇主报告您的求职行为</b
>如果您在使用过程中遇上程序错误您可以点击程序左下角进行反馈
本程序尊重您的隐私<b color-red
>不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及上级 / HRBP
团队报告您的求职行为不会向猎头公司泄露您的信息</b
>
</ElCheckbox>
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
<b class="color-red">本程序不对您的求职过程与结果负责</b
>为您开聊的职位均在Boss直聘上发布<b class="color-red">自行甄别为您开聊的公司</b
><b class="color-red">认真决定是否参加面试</b><b class="color-red">慎重选择Offer</b
本程序<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' : '']">
请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b>
本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在Boss直聘上发布职位信息真实性由Boss直聘负责<b
class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
>
</ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行如果您有顾虑建议通过VMWare
WorkStation/FusionVirtualBoxHyper-V/Windows沙盒等虚拟化技术运行本程序如发生问题请点击程序左下角进行反馈
请在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>
@@ -86,26 +112,36 @@
<script lang="ts" setup>
import { ElCheckbox, ElCheckboxGroup, ElMessage } from 'element-plus'
import { ref, onMounted, onBeforeMount } from 'vue';
import { gtagRenderer } from '@renderer/utils/gtag'
import { ref, onMounted, onBeforeMount } from 'vue'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'first-run-agreement',
...params
})
}
const electron = window.electron
const readmeItemCheckStatusList = ref<number[]>([])
const handleCancel = () => {
const handleCancel = async () => {
gtagRenderer('cancel_clicked')
await sleep(500)
electron.ipcRenderer.invoke('exit-app-immediately')
}
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
const handleSubmit = () => {
gtagRenderer('submit_clicked')
const COUNT = 9
const COUNT = 10
if (readmeItemCheckStatusList.value.length !== COUNT) {
gtagRenderer('agreement_not_finish_read_tip_displayed')
ElMessage.warning(
`您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`
)
ElMessage.warning({
message: `您还有${COUNT - readmeItemCheckStatusList.value.length}条没有读完,读完就打勾标记一下吧`,
grouping: true
})
unreadItemsAfterClickSubmit.value = {}
for (let i = 0; i < COUNT; i++) {
if (!readmeItemCheckStatusList.value.includes(i)) {
@@ -127,13 +163,10 @@ 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,
}
)
electron.ipcRenderer.send('update-window-size', {
width: componentRootEl.value!.offsetWidth,
height: componentRootEl.value!.offsetHeight
})
})
ro.observe(componentRootEl.value!)
onBeforeMount(() => {

View File

@@ -254,7 +254,7 @@ import {
} from 'element-plus'
import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue'
import { ref, onMounted, watch, nextTick, computed } from 'vue'
import { gtagRenderer } from '@renderer/utils/gtag'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../../common/constant'
import { v4 as uuid } from 'uuid'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
@@ -267,6 +267,13 @@ interface LlmConfigItem {
enabled: true
}
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'llm-config',
...params
})
}
function getNewConfigItem(): LlmConfigItem {
return {
id: uuid(),

View File

@@ -17,6 +17,95 @@
</div>
</el-form-item>
</el-card>
<el-card class="config-section">
<el-form-item class="job-source-form-item" prop="__jobSourceList">
<div w-full>
<div ref="jobSourceFormItemSectionEl" font-size-16px>
<div>
职位来源及其查找顺序
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_job_source_ui')"
>
<template #content>
<div m0 line-height-1.5em w-fit-content>
<img block h-320px src="../resources/intro-of-job-source.png" />
</div>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>下方展示的各条目与Boss直聘界面对应关系是怎样的</el-button
>
</el-tooltip>
</div>
</div>
<div font-size-12px>
拖放条目前方的手柄以调整职位来源查找顺序点击条目前方的开关以启用/禁用对应的职位来源
</div>
<JobSourceDragOrderer
v-model="formContent.__jobSourceList"
class="job-source-drag-orderer"
/>
</div>
</el-form-item>
</el-card>
<el-card class="config-section">
<el-form-item prop="filter">
<div font-size-16px>
职位筛选条件
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_wrongly_mark_not_suit')"
>
<template #content>
<ul m0 line-height-1.5em w-540px pl2em>
<li>
这个配置决定了下面列表中会展示哪些职位查找职位时会在Boss直聘页面上自动组合下方设置的筛选条件对应UI<br />
<img h-270px src="../resources/intro-of-job-filter-auto-change.png" />
</li>
<li color-orange>
建议不要筛选过多条件否则最终组合数会成为一个很大的数字这将在当前职位中尝试太多筛选条件不能及时进入下一个职位且会增加命中风控的概率
</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>这个配置是如何工作的</el-button
>
</el-tooltip>
</div>
<AnyCombineBossRecommendFilter v-model="formContent.anyCombineRecommendJobFilter" />
</el-form-item>
<el-form-item prop="filter" mb0>
<el-checkbox
v-if="anyCombineBossRecommendFilterHasCondition"
v-model="formContent.isSkipEmptyConditionForCombineRecommendJobFilter"
@change="
(v) => {
gtagRenderer('is_skip_empty_condition_4crjf_changed', { v })
}
"
>
<span font-size-12px>跳过初始空条件直接使用设置的条件查找职位</span>
</el-checkbox>
<el-checkbox v-else :model-value="false" disabled>
<span font-size-12px>跳过初始空条件直接使用设置的条件查找职位</span>
</el-checkbox>
</el-form-item>
<div font-size-12px mt10px>
当前组合条件数{{
currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString()
}}
<span
v-if="currentAnyCombineRecommendJobFilterCombinationCount >= 5"
class="color-orange"
>不建议选择太多组合条件 -
否则将在当前职位中尝试太多筛选条件不能及时进入下一个职位且会增加命中风控的概率</span
>
</div>
</el-card>
<!-- <el-form-item
label="钉钉机器人 AccessToken用于记录开聊请勿使用公司内部群"
prop="dingtalkRobotAccessToken"
@@ -520,50 +609,6 @@
><span><QuestionFilled w-1em h-1em mr2px /></span>如下各信息位置图示</el-button
>
</el-tooltip>
<el-tooltip
effect="light"
placement="bottom"
@show="gtagRenderer('tooltip_show_about_how_to_fill_df')"
>
<template #content>
<p style="margin-top: 4px; margin-bottom: 4px">
目前版本中,职位名称正则、职位类型正则、职位描述正则三个筛选条件,为“且”的关系。<br />
也就是说,职位信息中只要任一不能匹配到,即这个职位会被标记不合适。<br />
你可以留空某个输入框,这表示任何职位一定匹配这个条件。<br />
如果三个输入框均留空,表示列表中出现的任意职位一定同时匹配这三个条件。
</p>
因此,可以按照如下场景填写你对于期望职位的筛选条件:
<ul style="margin-top: 4px; margin-bottom: 4px">
<li>
如果你只考虑工作类型,请填写“职位类型正则”输入框,其余两个输入框清空。<br />
这可以确保求职方向基本正确。
</li>
<li>
如果你着重关注职位描述,请填写“职位描述正则”,其余两个输入框酌情填写。
</li>
<li>如果你想开聊列表里的推荐的任意职位,请清空这三个输入框。</li>
</ul>
<div>
你可以在左侧"职位详情筛选模板"选择一个模板,并在选中模板基础上尝试修改
</div>
<div>
<b>“职位类型正则”填写过程中请注意</b
>“职位类型”是由Boss直聘预定义好的一系列职位分类<br />
因此<b>请按照这个分类编写正则来进行填写</b>。这个分类可以在此找到:<br />
<img w-400px src="../resources/job-type-source-entry.png" />
</div>
<a
href="javascript:;"
style="color: var(--el-color-primary)"
@click.prevent="handleHowToFillDetailFilterClick"
>请参阅这个链接</a
>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>怎样填写下面的三个输入框?</el-button
>
</el-tooltip>
</div>
<div>
<el-dropdown ml20px @command="handleExpectJobFilterTemplateClicked">
@@ -586,69 +631,198 @@
</div>
<div
:style="{
display: 'grid',
gridTemplateColumns: '1fr 1em 1fr 1em 1fr',
gap: '5px',
width: '100%',
alignItems: 'end'
display: 'flex',
gap: '10px'
}"
class="job-detail-filter-wrap"
>
<el-form-item mb0 prop="expectJobNameRegExpStr">
<div font-size-12px>职位名称正则(不区分大小写)</div>
<el-input
v-model="formContent.expectJobNameRegExpStr"
placeholder="true"
@blur="
formContent.expectJobNameRegExpStr =
formContent.expectJobNameRegExpStr?.trim() ?? ''
"
<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'
}"
/>
</el-form-item>
<div mb10px font-size-12px flex flex-justify-center fw-800>且</div>
<el-form-item mb0 prop="expectJobTypeRegExpStr">
<div font-size-12px>职位类型正则(推荐填写,不区分大小写)</div>
<el-input
v-model="formContent.expectJobTypeRegExpStr"
placeholder="true"
@blur="
formContent.expectJobTypeRegExpStr =
formContent.expectJobTypeRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
<div mb10px font-size-12px flex flex-justify-center fw-800>且</div>
<el-form-item mb0 prop="expectJobDescRegExpStr">
<div font-size-12px>职位描述正则(不区分大小写)</div>
<el-input
v-model="formContent.expectJobDescRegExpStr"
placeholder="true"
@blur="
formContent.expectJobDescRegExpStr =
formContent.expectJobDescRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
</div>
<template
v-if="
formContent.expectJobNameRegExpStr?.trim() ||
formContent.expectJobTypeRegExpStr?.trim() ||
formContent.expectJobDescRegExpStr?.trim()
"
>
<div class="mt10px lh-2em font-size-12px">
当前职位名称/类型/描述不符合投递条件时:
<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().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().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().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().logicText }}
</div>
<el-form-item mb0 prop="expectJobDescRegExpStr">
<div font-size-12px>职位描述正则(不区分大小写)</div>
<el-input
v-model="formContent.expectJobDescRegExpStr"
type="textarea"
:placeholder="getJobDetailRegExpMatchLogicConfig().inputPlaceholderText"
:autosize="{ minRows: 2 }"
max-h-6lh
@blur="
formContent.expectJobDescRegExpStr =
formContent.expectJobDescRegExpStr?.trim() ?? ''
"
/>
</el-form-item>
</div>
</div>
<div
v-if="!isJobDetailRegExpEmpty()"
:style="{
display: 'grid',
gridTemplateColumns: '1.25fr 0.75fr',
gap: '10px 0',
width: '100%',
alignItems: 'end'
width: '400px',
borderLeft: '1px solid #f0f0f0',
paddingLeft: '10px',
flex: `0 0 auto`
}"
>
<div class="mt10px lh-2em font-size-12px">
当前职位名称/类型/描述不符合投递条件时:
</div>
<el-form-item mb0>
<el-select
v-model="formContent.jobNotMatchStrategy"
@@ -663,9 +837,17 @@
>
</el-select>
</el-form-item>
<div />
</div>
</template>
<div
v-else
:style="{
width: '400px',
borderLeft: '1px solid transparent',
paddingLeft: '10px',
flex: `0 0 auto`
}"
/>
</div>
</div>
<div class="h-1px bg-#f0f0f0" mt16px mb16px />
<div mt16px>
@@ -717,39 +899,6 @@
</div>
</div>
</el-card>
<el-card class="config-section">
<el-form-item prop="filter" mb0>
<div font-size-16px>
职位备选筛选条件
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_wrongly_mark_not_suit')"
>
<template #content>
<ul m0 line-height-1.5em w-400px pl2em>
<li>当前求职期望无合适职位时自动更改Boss直聘页面上的筛选条件查找新工作</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>这个配置是如何工作的?</el-button
>
</el-tooltip>
</div>
<AnyCombineBossRecommendFilter v-model="formContent.anyCombineRecommendJobFilter" />
<div font-size-12px>
当前组合条件数:{{
currentAnyCombineRecommendJobFilterCombinationCount.toLocaleString()
}}
<span
v-if="currentAnyCombineRecommendJobFilterCombinationCount >= 20"
class="color-orange"
>不建议选择太多组合条件</span
>
</div>
</el-form-item>
</el-card>
</el-form>
</div>
<div class="bg-#f8f8f8 pb10px pt10px">
@@ -777,19 +926,31 @@ import { QuestionFilled } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import AnyCombineBossRecommendFilter from '@renderer/features/AnyCombineBossRecommendFilter/index.vue'
import { activeDescList } from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs'
import { calculateTotalCombinations } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
import { gtagRenderer } from '@renderer/utils/gtag'
import {
calculateTotalCombinations,
checkAnyCombineBossRecommendFilterHasCondition
} from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import defaultTargetCompanyListConf from '@geekgeekrun/geek-auto-start-chat-with-boss/default-config-file/target-company-list.json'
import { ArrowDown } from '@element-plus/icons-vue'
import {
MarkAsNotSuitOp,
StrategyScopeOptionWhenMarkJobNotMatch,
SalaryCalculateWay
SalaryCalculateWay,
JobDetailRegExpMatchLogic
} from '@geekgeekrun/sqlite-plugin/src/enums'
import { debounce } from 'lodash-es'
import mittBus from '../../../utils/mitt'
import CityChooser from './components/CityChooser.vue'
import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-conditions-20241002.json'
import JobSourceDragOrderer from '../../../features/JobSourceDragOrderer/index.vue'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'gascwb-config',
...params
})
}
const router = useRouter()
@@ -819,12 +980,45 @@ const formContent = ref({
expectWorkExpList: [],
expectWorkExpNotMatchStrategy: MarkAsNotSuitOp.NO_OP,
strategyScopeOptionWhenMarkJobWorkExpNotMatch:
StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB,
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY,
isSkipEmptyConditionForCombineRecommendJobFilter: false,
__jobSourceList: formatJobSourceConfigToFormValue([
{
type: 'expect',
enabled: true
}
])
})
const anyCombineBossRecommendFilterHasCondition = computed(() => {
return checkAnyCombineBossRecommendFilterHasCondition(
formContent.value.anyCombineRecommendJobFilter
)
})
const currentAnyCombineRecommendJobFilterCombinationCount = computed(() => {
return calculateTotalCombinations(formContent.value.anyCombineRecommendJobFilter)
return calculateTotalCombinations(
formContent.value.anyCombineRecommendJobFilter,
anyCombineBossRecommendFilterHasCondition.value
? !formContent.value.isSkipEmptyConditionForCombineRecommendJobFilter
: true
)
})
watch(
() => currentAnyCombineRecommendJobFilterCombinationCount.value,
(v) => {
const allCountMap = {}
Object.entries(formContent.value.anyCombineRecommendJobFilter).forEach(([k, v]) => {
allCountMap[k + 'Length'] = v?.length
})
gtagRenderer('any_combine_rjfc_count', {
combinedAllCount: v,
...allCountMap
})
}
)
const unwatchAnyCombineRecommendJobFilter = ref<null | (() => void)>(null)
onBeforeUnmount(() => {
@@ -919,8 +1113,17 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
formContent.value.strategyScopeOptionWhenMarkJobWorkExpNotMatch =
res.config['boss.json'].strategyScopeOptionWhenMarkJobWorkExpNotMatch ??
StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB
formContent.value.jobDetailRegExpMatchLogic =
res.config['boss.json'].jobDetailRegExpMatchLogic ?? JobDetailRegExpMatchLogic.EVERY
formContent.value.isSkipEmptyConditionForCombineRecommendJobFilter =
res.config['boss.json'].isSkipEmptyConditionForCombineRecommendJobFilter ?? false
formContent.value.__jobSourceList = formatJobSourceConfigToFormValue(
res.config['boss.json'].jobSourceList || []
)
})
const jobSourceFormItemSectionEl = ref()
const jobDetailRegExpSectionEl = ref()
const formRules = {
expectJobNameRegExpStr: {
validator(_, value, cb) {
@@ -935,6 +1138,7 @@ const formRules = {
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_name')
}
}
@@ -952,6 +1156,7 @@ const formRules = {
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_type')
}
}
@@ -969,9 +1174,41 @@ const formRules = {
cb()
} catch (err) {
cb(new Error(`正则无效:${err?.message}`))
jobDetailRegExpSectionEl.value?.scrollIntoViewIfNeeded()
gtagRenderer('invalid_reg_exp_for_expect_job_desc')
}
}
},
__jobSourceList: {
trigger: null,
validator(_, value, cb) {
if (!Array.isArray(value)) {
cb()
return
}
if (value.every((it) => !it.enabled)) {
cb(new Error(`将上方任一来源设置为“启用”后才能继续`))
return
}
const configMap = {}
for (const config of value) {
if (configMap[config.type]) {
continue
}
configMap[config.type] = config
}
if (
!configMap.expect.enabled &&
!configMap.recommend.enabled &&
configMap.search.enabled &&
!configMap.search?.children?.some((it) => it.enabled && it.keyword?.trim())
) {
cb(new Error(`将上方任一来源设置为“启用”后才能继续`))
jobSourceFormItemSectionEl.value?.scrollIntoViewIfNeeded()
return
}
cb()
}
}
}
@@ -991,7 +1228,12 @@ const handleSubmit = async () => {
console.log(err)
return
}
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
const clonedFormContent = JSON.parse(JSON.stringify(formContent.value))
clonedFormContent.jobSourceList = formatJobSourceFormValueToConfig(
clonedFormContent.__jobSourceList
)
delete clonedFormContent.__jobSourceList
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(clonedFormContent))
mittBus.emit('auto-start-chat-with-boss-config-saved')
router.replace({
path: '/geekAutoStartChatWithBoss/prepareRun',
@@ -1016,7 +1258,12 @@ const handleSave = async () => {
console.log(err)
return
}
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
const clonedFormContent = JSON.parse(JSON.stringify(formContent.value))
clonedFormContent.jobSourceList = formatJobSourceFormValueToConfig(
clonedFormContent.__jobSourceList
)
delete clonedFormContent.__jobSourceList
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(clonedFormContent))
mittBus.emit('auto-start-chat-with-boss-config-saved')
ElMessage.success('配置保存成功')
gtagRenderer('config_saved')
@@ -1081,7 +1328,8 @@ const expectJobFilterTemplateList = [
config: {
expectJobNameRegExpStr: '',
expectJobTypeRegExpStr: '',
expectJobDescRegExpStr: ''
expectJobDescRegExpStr: '',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
@@ -1089,7 +1337,8 @@ const expectJobFilterTemplateList = [
config: {
expectJobNameRegExpStr: '前端|H5|FE',
expectJobTypeRegExpStr: '前端开发|javascript',
expectJobDescRegExpStr: '前端|vue|react|node|js|javascript|H5'
expectJobDescRegExpStr: '前端|vue|react|node|js|javascript|H5',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
@@ -1097,7 +1346,8 @@ const expectJobFilterTemplateList = [
config: {
expectJobNameRegExpStr: '\\bJava\\b',
expectJobTypeRegExpStr: '\\bJava\\b',
expectJobDescRegExpStr: '\\bJava\\b|JVM|消息队列|MQ|MySQL|Nginx|Redis|Dubbo'
expectJobDescRegExpStr: '\\bJava\\b|JVM|消息队列|MQ|MySQL|Nginx|Redis|Dubbo',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
@@ -1105,7 +1355,8 @@ const expectJobFilterTemplateList = [
config: {
expectJobNameRegExpStr: '员工关系|劳动关系|SSC|人力资源|人资',
expectJobTypeRegExpStr: '员工关系|人力资源',
expectJobDescRegExpStr: '社保|考勤|入职|离职'
expectJobDescRegExpStr: '社保|考勤|入职|离职',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
},
{
@@ -1113,7 +1364,8 @@ const expectJobFilterTemplateList = [
config: {
expectJobNameRegExpStr: '招聘|招聘HR|招聘专员|招聘顾问|招聘专家|Recruiter|人力资源|人资',
expectJobTypeRegExpStr: '招聘|人力资源|猎头顾问',
expectJobDescRegExpStr: '简历筛选|面试安排|offer|猎头'
expectJobDescRegExpStr: '简历筛选|面试安排|offer|猎头',
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.SOME
}
}
]
@@ -1153,6 +1405,17 @@ const strategyScopeOptionWhenMarkJobNotMatch = [
}
]
const jobDetailRegExpMatchLogicOptions = [
{
name: '“且”模式 - 所有正则匹配时才认为职位匹配',
value: JobDetailRegExpMatchLogic.EVERY
},
{
name: '“或”模式 - 任一正则匹配时即认为职位匹配',
value: JobDetailRegExpMatchLogic.SOME
}
]
async function handleExpectSalaryCalculateWayChanged(value) {
gtagRenderer('expect_salary_calculate_way_changed', { value })
@@ -1275,6 +1538,87 @@ function handleHowToFillDetailFilterClick() {
'https://linux.do/t/topic/640626/74?u=geekgeekrun'
)
}
function isJobDetailRegExpEmpty() {
return [
formContent.value.expectJobDescRegExpStr,
formContent.value.expectJobNameRegExpStr,
formContent.value.expectJobTypeRegExpStr
]
.map((it) => Boolean(it?.trim()))
.every((it) => it === false)
}
function getJobDetailRegExpMatchLogicConfig() {
const result = {
logicText: '-',
inputPlaceholderText: '-'
}
if (formContent.value.jobDetailRegExpMatchLogic === JobDetailRegExpMatchLogic.EVERY) {
Object.assign(result, {
logicText: '且',
inputPlaceholderText: 'true'
})
}
if (formContent.value.jobDetailRegExpMatchLogic === JobDetailRegExpMatchLogic.SOME) {
Object.assign(result, {
logicText: '或',
inputPlaceholderText: 'false'
})
}
if (isJobDetailRegExpEmpty()) {
result.inputPlaceholderText = 'true'
}
return result
}
function formatJobSourceConfigToFormValue(config = []) {
const typeToNameKey = {
recommend: '推荐列表中的职位',
expect: '根据设置的求职期望推荐的职位',
search: '通过搜索找到的职位'
}
const isInitEmpty = !config?.length
config = config.filter((it) => Object.hasOwn(typeToNameKey, it.type))
const addedSet = new Set()
const tempArr = []
config.forEach((it) => {
if (!Object.hasOwn(typeToNameKey, it.type)) {
return
}
tempArr.push(it)
addedSet.add(it.type)
})
config = tempArr
Object.keys(typeToNameKey).forEach((k) => {
if (addedSet.has(k)) {
return
}
// handle init value
tempArr.push({
type: k,
enabled: isInitEmpty && k === 'expect'
})
addedSet.add(k)
})
return config.map((outerItem) => {
return {
...outerItem,
label: typeToNameKey[outerItem.type]
}
})
}
function formatJobSourceFormValueToConfig(formValue = []) {
return formValue.map((it) => {
it = { ...it }
delete it.label
return it
})
}
</script>
<style scoped lang="scss">
@@ -1325,12 +1669,6 @@ function handleHowToFillDetailFilterClick() {
--el-slider-button-size: 18px;
}
}
.job-detail-filter-wrap {
transition: 0.2s ease margin-bottom;
&:has(.el-form-item__error) {
margin-bottom: 40px;
}
}
</style>
<style lang="scss">
@@ -1339,5 +1677,29 @@ function handleHowToFillDetailFilterClick() {
font-size: 12px;
line-height: 1.2em;
}
.job-detail-filter-wrap .el-form-item__error {
position: static;
word-break: break-word;
}
.job-source-form-item {
margin-bottom: 0;
.job-source-drag-orderer {
margin-top: 10px;
background-color: #fff;
padding: 20px;
border: 1px solid var(--el-card-border-color);
border-radius: 4px;
}
&.is-error {
margin-bottom: 18px;
.el-input__wrapper {
box-shadow: 0 0 0 1px var(--el-border-color) inset;
}
.job-source-drag-orderer {
background-color: var(--el-color-danger-light-9);
border: 1px solid var(--el-color-danger);
}
}
}
}
</style>

View File

@@ -231,10 +231,15 @@ import {
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK
} from '../../../../common/enums/auto-start-chat'
import { gtagRenderer } from '@renderer/utils/gtag'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import mittBus from '../../utils/mitt'
import { QuestionFilled } from '@element-plus/icons-vue'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'rnrr-config',
...params
})
}
const router = useRouter()
const formContent = ref({
autoReminder: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

View File

@@ -322,11 +322,18 @@
import { ElForm, ElButton, ElAlert, ElMessageBox } from 'element-plus'
import { ref, onMounted, computed } from 'vue'
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
import { gtagRenderer } from '@renderer/utils/gtag'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { type ResumeContent, resumeContentEnoughDetect } from '../../../../common/utils/resume'
const formRef = ref<InstanceType<typeof ElForm>>()
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'resume-editor',
...params
})
}
const getEmptyFormContent = () => {
const o: any = {
expectJob: '',

16
pnpm-lock.yaml generated
View File

@@ -150,6 +150,9 @@ importers:
uuid:
specifier: ^11.1.0
version: 11.1.0
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.4.15)
devDependencies:
'@electron-toolkit/eslint-config':
specifier: ^1.0.2
@@ -5821,6 +5824,10 @@ packages:
ip: 2.0.0
smart-buffer: 4.2.0
/sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
dev: false
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
@@ -6625,6 +6632,15 @@ packages:
'@vue/shared': 3.4.15
typescript: 5.3.3
/vuedraggable@4.1.0(vue@3.4.15):
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
peerDependencies:
vue: ^3.0.1
dependencies:
sortablejs: 1.14.0
vue: 3.4.15(typescript@5.3.3)
dev: false
/web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}