mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-07-20 04:02:31 +08:00
Merge branch 'feature/ui'
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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/Fusion、VirtualBox、Hyper-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 / Fusion、Oracle VirtualBox、Microsoft 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(() => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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
16
pnpm-lock.yaml
generated
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user