mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-05-06 20:02:47 +08:00
add core logic for job source
This commit is contained in:
@@ -23,5 +23,11 @@
|
||||
"recentMessageQuantityForLlm": 8,
|
||||
"rechatLlmFallback": 1,
|
||||
"onlyRemindBossWithExpectJobType": true
|
||||
}
|
||||
},
|
||||
"jobSourceList": [
|
||||
{
|
||||
"type": "expect",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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">
|
||||
添加一个关键词后方可启用->
|
||||
</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>
|
||||
@@ -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
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