add core logic for job source

This commit is contained in:
geekgeekrun
2025-07-12 19:31:14 +08:00
parent b89d6e264d
commit f078500c14
7 changed files with 409 additions and 30 deletions

View File

@@ -23,5 +23,11 @@
"recentMessageQuantityForLlm": 8,
"rechatLlmFallback": 1,
"onlyRemindBossWithExpectJobType": true
}
},
"jobSourceList": [
{
"type": "expect",
"enabled": true
}
]
}

View File

@@ -152,6 +152,54 @@ if (
jobDetailRegExpMatchLogic = JobDetailRegExpMatchLogic.EVERY
}
let {
jobSourceList
} = readConfigFile('boss.json')
if (!jobSourceList?.length) {
jobSourceList = [
{
type: "expect",
enabled: true
}
]
}
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)
}
}
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
const recommendJobPageUrl = `https://www.zhipin.com/web/geek/jobs`
@@ -443,23 +491,6 @@ async function setFilterCondition (selectedFilters) {
}
}
const jobSource = [
{
name: 'recommendJob'
},
{
name: 'userSetExpectJob'
},
{
name: 'searchJob',
keyword: 'HRBP'
},
{
name: 'searchJob',
keyword: '招聘'
}
]
async function toRecommendPage (hooks) {
let userInfoPromise = page.waitForResponse((response) => {
if (response.url().startsWith('https://www.zhipin.com/wapi/zpuser/wap/getUserInfo.json')) {
@@ -512,11 +543,11 @@ async function toRecommendPage (hooks) {
const SEARCH_BOX_SELECTOR = `.c-search-input .search-input-box`
const computedSourceList = []
for (const source of jobSource) {
switch (source.name) {
case 'recommendJob': {
for (const source of normalizedJobSource) {
switch (source.type) {
case 'recommend': {
computedSourceList.push({
sourceName: source.name,
type: source.type,
selector: RECOMMEND_JOB_ENTRY_SELECTOR,
async getIsCurrentActiveSource () {
return await page.evaluate(
@@ -536,12 +567,12 @@ async function toRecommendPage (hooks) {
})
continue
}
case 'userSetExpectJob': {
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({
sourceName: source.name,
type: source.type,
selector: `${USER_SET_EXPECT_JOB_ENTRIES_SELECTOR}:nth-child(${index + 1})`,
async getIsCurrentActiveSource () {
return await page.evaluate(
@@ -566,9 +597,9 @@ async function toRecommendPage (hooks) {
})
break
}
case 'searchJob': {
case 'search': {
computedSourceList.push({
sourceName: source.name,
type: source.type,
async getIsCurrentActiveSource () {
const elHandle = await page.$(`.page-jobs-main`)
const currentKeyWord = await elHandle?.evaluate((el) => {

View File

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

View File

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

View File

@@ -0,0 +1,245 @@
<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"
>
<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.item.type === 'search' &&
(!element.item?.children?.length ||
!element.item?.children?.some((it) => it.enabled) ||
!element.item?.children?.some((it) => !!it.keyword?.trim()))
"
>
<el-switch
model-value="false"
mr10px
disabled
active-text="启用"
inactive-text="禁用"
inline-prompt
/>
{{ element.label }}
</template>
<template v-else>
<el-switch
v-model="element.item.enabled"
mr10px
active-text="启用"
inactive-text="禁用"
inline-prompt
/>
{{ element.label }}
</template>
</div>
<div v-if="element.item.type === 'search'">
<div flex flex-items-center>
<span color-orange align-self-end mr10px>
<template v-if="!element.item?.children?.length">
添加一个关键词后方可启用-&gt;
</template>
<template
v-else-if="
element.item?.children?.every((it) => !(it.enabled && !!it.keyword?.trim()))
"
>
至少启用下方任意一个不为空的关键词后方可启用
</template>
<template
v-else-if="element.item?.children?.some((it) => it.enabled && !it.keyword?.trim())"
>
留空的关键词会被跳过
</template>
</span>
<el-button p-0 h-fit type="text" @click="addSearchKeyword(element.item)"
>添加关键词</el-button
>
</div>
</div>
</span>
</div>
<div v-if="element.item.type === 'search' && element.item?.children?.length">
<draggable
v-model="element.item.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"
>
<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.item.enabled"
v-model="searchItem.enabled"
mr10px
active-text="启用"
inactive-text="禁用"
inline-prompt
/>
<el-switch
v-else
disabled
:model-value="false"
mr10px
active-text="启用"
inactive-text="禁用"
inline-prompt
/>
<el-input
v-model="searchItem.keyword"
maxlength="100"
@blur="() => (searchItem.keyword = searchItem.keyword?.trim() ?? '')"
/>
</div>
<el-button
p-0
ml-10px
h-fit
type="danger"
link
@click="removeSearchKeywordByIndex(element.item, index)"
>删除</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'
defineProps({
modelValue: {
type: Array
}
})
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.children.push({
type: 'search-kw',
enabled: true,
keyword: ''
})
}
function removeSearchKeywordByIndex(item, index) {
item.children?.splice(index, 1)
}
</script>
<style lang="scss" scoped>
.ghost {
opacity: 0.5;
filter: blur(2px);
}
.list-group {
line-height: 32px;
min-height: 20px;
.list-group {
margin-left: 20px;
}
.list-group-item {
background-color: #fff;
list-style: none;
display: flex;
flex-direction: column;
padding-top: 6px;
padding-bottom: 6px;
padding-right: 10px;
border: 1px solid var(--el-card-border-color);
margin-top: 6px;
margin-bottom: 6px;
box-shadow: var(--el-box-shadow-light);
border-radius: 4px;
.inner-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
::v-deep(.el-checkbox) {
.el-checkbox__label {
width: 100%;
}
}
}
.drag-handle-wrap {
display: flex;
justify-content: flex-end;
width: 25px;
height: 1em;
cursor: grab;
margin-right: 4px;
.drag-handle {
width: 20px;
background-image: linear-gradient(
90deg,
transparent 35%,
var(--el-card-border-color) 35%,
var(--el-card-border-color) 45%,
transparent 45%,
transparent 55%,
var(--el-card-border-color) 55%,
var(--el-card-border-color) 65%,
transparent 65%,
transparent 100%
);
}
}
}
}
</style>

View File

@@ -17,6 +17,14 @@
</div>
</el-form-item>
</el-card>
<el-card class="config-section">
<el-form-item prop="filter" mb0>
<div w-full>
<div font-size-16px>职位来源及查找顺序</div>
<JobSourceDragOrderer v-model="formContent.__jobSourceList" />
</div>
</el-form-item>
</el-card>
<el-card class="config-section">
<el-form-item prop="filter">
<div font-size-16px>
@@ -885,6 +893,7 @@ 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 router = useRouter()
@@ -917,7 +926,13 @@ const formContent = ref({
StrategyScopeOptionWhenMarkJobNotMatch.ONLY_COMPANY_MATCHED_JOB,
jobDetailRegExpMatchLogic: JobDetailRegExpMatchLogic.EVERY,
isSkipEmptyConditionForCombineRecommendJobFilter: false
isSkipEmptyConditionForCombineRecommendJobFilter: false,
__jobSourceList: formatJobSourceConfigToFormValue([
{
type: 'expect',
enabled: true
}
])
})
const anyCombineBossRecommendFilterHasCondition = computed(() => {
@@ -1032,6 +1047,9 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
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 formRules = {
@@ -1104,7 +1122,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',
@@ -1129,7 +1152,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')
@@ -1438,6 +1466,55 @@ function getJobDetailRegExpMatchLogicConfig() {
}
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 {
item: outerItem,
label: typeToNameKey[outerItem.type],
children:
outerItem.children && outerItem.type === 'search'
? (outerItem.children || []).map((innerItem) => ({
item: innerItem
}))
: null
}
})
}
function formatJobSourceFormValueToConfig(formValue = []) {
return formValue.map((it) => it.item)
}
</script>
<style scoped lang="scss">

16
pnpm-lock.yaml generated
View File

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