mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-07-12 07:52:18 +08:00
Compare commits
58 Commits
ui-v0.10.2
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa6527432d | ||
|
|
64f9ddfb76 | ||
|
|
6b131e708c | ||
|
|
1e9f436633 | ||
|
|
0c815f2557 | ||
|
|
b8753597da | ||
|
|
98300ddf66 | ||
|
|
962dcb1125 | ||
|
|
a9823f1e24 | ||
|
|
57eb09ed49 | ||
|
|
a8454cbaac | ||
|
|
ddc8a81c7c | ||
|
|
ca37e258c6 | ||
|
|
454ee421bf | ||
|
|
69bd774640 | ||
|
|
20f081a245 | ||
|
|
252869576b | ||
|
|
98ab2acf50 | ||
|
|
dbc127be06 | ||
|
|
82f19d772d | ||
|
|
94de507c9a | ||
|
|
c1faecaaed | ||
|
|
880557c1ad | ||
|
|
3b318262a4 | ||
|
|
f9786d5d82 | ||
|
|
b583951cb5 | ||
|
|
d95abd3ad1 | ||
|
|
2e9d3af276 | ||
|
|
fbeb01ab48 | ||
|
|
6db6ef4972 | ||
|
|
7b3390745a | ||
|
|
b2b1cecfa9 | ||
|
|
61d7d968a1 | ||
|
|
c18930f76e | ||
|
|
fc13742d52 | ||
|
|
af05a5555e | ||
|
|
bc57a07a0a | ||
|
|
265f2f89e5 | ||
|
|
206d50ca61 | ||
|
|
9324f20ad3 | ||
|
|
632a11b105 | ||
|
|
418ca66a30 | ||
|
|
0928b402fb | ||
|
|
373e45dee8 | ||
|
|
8bbfa5b41e | ||
|
|
aa410cfc60 | ||
|
|
e986fddbfb | ||
|
|
cf7a0d3902 | ||
|
|
c95349c037 | ||
|
|
8197782102 | ||
|
|
f17d626ea0 | ||
|
|
2b4b620977 | ||
|
|
03e3c3661f | ||
|
|
5d787fdd50 | ||
|
|
b68ff63b6b | ||
|
|
2aee76307f | ||
|
|
e2040caa38 | ||
|
|
fba45e420f |
3
.github/workflows/release-ui.yml
vendored
3
.github/workflows/release-ui.yml
vendored
@@ -149,9 +149,10 @@ jobs:
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -llR geekgeekrun-ui@${{ github.sha }}
|
||||
|
||||
- name: Create release
|
||||
- name: Create release for public
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
repo: geekgeekrun
|
||||
prerelease: true
|
||||
allowUpdates: true
|
||||
artifacts: geekgeekrun-ui@${{ github.sha }}/*
|
||||
|
||||
16
README.md
16
README.md
@@ -1,5 +1,9 @@
|
||||
# 牛人快跑 - GeekGeekRun
|
||||
|
||||
**->由于BOSS直聘于2026.1.8上线了反调试/反爬虫/反自动化逻辑,本程序可能暂时无法使用,目前正在尝试处理中;有相关处理经验的朋友可以提交PR<-**
|
||||
|
||||
**->维护者正在疯狂找工作<-**
|
||||
|
||||
一款可以帮助你在Boss直聘上**自动批量开聊Boss**的脚本,基于Puppeteer。
|
||||
|
||||
与每一位牛人站在一起
|
||||
@@ -14,7 +18,7 @@
|
||||
### Boss炸弹
|
||||
扩列神器!按照你所设置的求职偏好,自动开聊推荐职位列表中的匹配的Boss。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位;可以通过拖拽的方式自定义搜索顺序哦~
|
||||
|
||||
<img width="1280" height="2474" alt="Snipaste_2025-07-15_13-05-35" src="https://github.com/user-attachments/assets/30308ec2-1e3b-4a8f-b3fe-7d205394578d" />
|
||||
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
|
||||
|
||||
|
||||
#### 匹配步骤
|
||||
@@ -136,10 +140,10 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
### 个人而言
|
||||
以我为例,我真的求职求累了
|
||||
|
||||
2023年初~2025年初,这段时间
|
||||
- 求职经历:离职求职5个月,在职求职3个月;曾面试66个岗位,各种面试(一面、二面、三面、HR面)加起来134次;Offer 寥寥无几
|
||||
- 被动失业经历:1 次裁员,1 次卡试用期
|
||||
- 目前状态:有工作(但愿明天也有工作)
|
||||
2023年初~2026年年初,这段时间
|
||||
- 求职经历:离职求职7个月;曾面试73个岗位,各种面试(一面、二面、三面、HR面)加起来156次;Offer 寥寥无几
|
||||
- 被动失业经历:1 次裁员,1 次卡试用期,1 次部门整体裁撤
|
||||
- 目前状态:无工作(**正在疯狂找工作**)
|
||||
|
||||
如此多的经历,已经让我成为了一位资深Boss用户
|
||||
|
||||
@@ -157,7 +161,7 @@ Boss不明原因已读不回?简历就是投不出去?
|
||||
一个岗位可以收到一堆简历投递,大部分简历最终的归宿都是人才库或者垃圾场
|
||||
|
||||
当然,如果运气好:
|
||||
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被恶心到,但你又没有其它更好 Offer ,不敢贸然放弃这个不满意的 Offer
|
||||
- 好不容易过了 HR 面,到了 Offer 阶段,HR / 用人部门 几乎都在极限压缩用人成本,经常能被 ** 到,但你又没有其它更好 Offer ,不敢贸然放弃这个不满意的 Offer
|
||||
- 终于你自我催眠(类似:“领导看我薪资低,所以一定会争取给我涨薪”、“领导看我薪资低,所以假设我加倍努力工作绩效一定会向我倾斜”、“领导画的饼很好,吃了一定能有好结果”),接了不满意的 Offer 入了职,然而经过一段时间的接触,最终结果完全不符合期望(诸如:“团队很难融入”、“协作方经常踢皮球”、“你需要做很多脏活累活,而且要帮前人擦屁股”、“领导希望用最小成本把你招来,这样裁员时可以最小成本把你打发走”、“团队需要新人来背C/M-/3.25/裁员指标”、“领导分配的工作和领导入职时画的诱人的饼完全不一致”)
|
||||
|
||||
为了避免这种情况的发生,找工作时还是需要尽可能多的面试,多个选择。如果真的不慎遭遇了这些情况,让你认为在当前岗位继续做下去的收益不如离职换工作的收益,那就做好走的准备吧
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
"author": "geekgeekrun",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"dayjs": "^1.11.10",
|
||||
"json5": "^2.2.3",
|
||||
"minimist": "^1.2.8",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer": "24.19.0",
|
||||
"puppeteer-extra": "3.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||
"rimraf": "^3.0.2",
|
||||
@@ -28,5 +30,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": "=8.15.9"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"find-chrome-bin@2.0.4": "patches/find-chrome-bin@2.0.4.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
export default {
|
||||
"code": 0,
|
||||
"message": "Success",
|
||||
"zpData": {
|
||||
@@ -56,10 +56,11 @@ function combineWithZero(arr, min, max) {
|
||||
}
|
||||
|
||||
export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
const { salaryList, experienceList, degreeList, scaleList, industryList } =
|
||||
const { cityList, salaryList, experienceList, degreeList, scaleList, industryList } =
|
||||
selectedFilters;
|
||||
|
||||
// 生成符合限制条件的组合
|
||||
const cityComb = combineWithZero(cityList, 0, 1) // Salary: 0-1 个
|
||||
const salaryComb = combineWithZero(salaryList, 0, 1) // Salary: 0-1 个
|
||||
const experienceComb = combineWithZero(experienceList, 0, experienceList.length) // Experience: 0 个或更多
|
||||
const degreeComb = combineWithZero(degreeList, 0, degreeList.length) // Degree: 0 个或更多
|
||||
@@ -67,17 +68,20 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
|
||||
|
||||
// 通过迭代生成所有组合
|
||||
for (const salary of salaryComb) {
|
||||
for (const experience of experienceComb) {
|
||||
for (const degree of degreeComb) {
|
||||
for (const scale of scaleComb) {
|
||||
for (const industry of industryComb) {
|
||||
yield {
|
||||
salaryList: salary,
|
||||
experienceList: experience,
|
||||
degreeList: degree,
|
||||
scaleList: scale,
|
||||
industryList: industry
|
||||
for (const city of cityComb) {
|
||||
for (const salary of salaryComb) {
|
||||
for (const experience of experienceComb) {
|
||||
for (const degree of degreeComb) {
|
||||
for (const scale of scaleComb) {
|
||||
for (const industry of industryComb) {
|
||||
yield {
|
||||
cityList: city,
|
||||
salaryList: salary,
|
||||
experienceList: experience,
|
||||
degreeList: degree,
|
||||
scaleList: scale,
|
||||
industryList: industry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +96,7 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
|
||||
// 计算符合限制条件的组合数量
|
||||
export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) {
|
||||
const {
|
||||
cityList = [],
|
||||
salaryList = [],
|
||||
experienceList = [],
|
||||
degreeList = [],
|
||||
@@ -100,13 +105,14 @@ export function calculateTotalCombinations(selectedFilters, includeEmptyConditio
|
||||
} = selectedFilters
|
||||
|
||||
// 生成符合限制条件的组合
|
||||
const cityComb = combineWithZero(cityList, 0, 1) // City: 0-1 个
|
||||
const salaryComb = combineWithZero(salaryList, 0, 1) // Salary: 0-1 个
|
||||
const experienceComb = combineWithZero(experienceList, 0, experienceList.length) // Experience: 0 个或更多
|
||||
const degreeComb = combineWithZero(degreeList, 0, degreeList.length) // Degree: 0 个或更多
|
||||
const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多
|
||||
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
|
||||
|
||||
let result = [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
|
||||
let result = [cityComb, salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
|
||||
return accu * cur.length
|
||||
}, 1)
|
||||
if (!includeEmptyCondition) {
|
||||
@@ -146,6 +152,7 @@ export function formatStaticCombineFilters(rawStaticCombineRecommendJobFilterCon
|
||||
const conditions = Array.from(map.values())
|
||||
const result = conditions.map((condition) => {
|
||||
return {
|
||||
cityList: condition.city ? [condition.city] : [],
|
||||
salaryList: condition.salary ? [condition.salary] : [],
|
||||
experienceList: condition.experience ? [condition.experience] : [],
|
||||
degreeList: condition.degree ? [condition.degree] : [],
|
||||
@@ -155,6 +162,7 @@ export function formatStaticCombineFilters(rawStaticCombineRecommendJobFilterCon
|
||||
})
|
||||
if (!result.length) {
|
||||
result.push({
|
||||
cityList: [],
|
||||
salaryList: [],
|
||||
experienceList: [],
|
||||
degreeList: [],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"combineRecommendJobFilterType": 1,
|
||||
"anyCombineRecommendJobFilter": {
|
||||
"cityList": [],
|
||||
"salaryList": [],
|
||||
"experienceList": [],
|
||||
"degreeList": [],
|
||||
@@ -31,5 +32,8 @@
|
||||
"type": "expect",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"isSageTimeEnabled": true,
|
||||
"sageTimeOpTimes": 100,
|
||||
"sageTimePauseMinute": 15
|
||||
}
|
||||
@@ -37,6 +37,19 @@ import {
|
||||
SEARCH_BOX_SELECTOR,
|
||||
} from './constant.mjs'
|
||||
import { parseSalary } from "@geekgeekrun/sqlite-plugin/dist/utils/parser"
|
||||
import { waitForSageTimeOrJustContinue } from './sage-time.mjs'
|
||||
import cityGroupData from './cityGroup.mjs'
|
||||
const flattedCityList = []
|
||||
;(cityGroupData?.zpData?.cityGroup ?? []).forEach(it => {
|
||||
const firstChar = it.firstChar
|
||||
it.cityList.forEach(city => {
|
||||
flattedCityList.push({
|
||||
...city,
|
||||
firstChar
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const jobFilterConditionsMapByCode = {}
|
||||
Object.values(jobFilterConditions).forEach(arr => {
|
||||
arr.forEach(option => {
|
||||
@@ -72,21 +85,26 @@ export const autoStartChatEventBus = new EventEmitter()
|
||||
*/
|
||||
let puppeteer
|
||||
let StealthPlugin
|
||||
let LaodengPlugin
|
||||
export async function initPuppeteer () {
|
||||
// production
|
||||
const importResult = await Promise.all(
|
||||
[
|
||||
import('puppeteer-extra'),
|
||||
import('puppeteer-extra-plugin-stealth')
|
||||
import('puppeteer-extra-plugin-stealth'),
|
||||
import('@geekgeekrun/puppeteer-extra-plugin-laodeng')
|
||||
]
|
||||
)
|
||||
puppeteer = importResult[0].default
|
||||
StealthPlugin = importResult[1].default
|
||||
LaodengPlugin = importResult[2].default
|
||||
puppeteer.use(StealthPlugin())
|
||||
puppeteer.use(LaodengPlugin())
|
||||
|
||||
return {
|
||||
puppeteer,
|
||||
StealthPlugin
|
||||
StealthPlugin,
|
||||
LaodengPlugin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +387,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
|
||||
let isJobNameSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||||
try {
|
||||
if (expectJobNameRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobNameRegExpStr, 'i')
|
||||
const regExp = new RegExp(expectJobNameRegExpStr, 'im')
|
||||
isJobNameSuit = regExp.test(jobInfo.jobName?.replace(/\n/g, '') ?? '')
|
||||
}
|
||||
} catch {
|
||||
@@ -377,7 +395,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
|
||||
let isJobTypeSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||||
try {
|
||||
if (expectJobTypeRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobTypeRegExpStr, 'i')
|
||||
const regExp = new RegExp(expectJobTypeRegExpStr, 'im')
|
||||
isJobTypeSuit = regExp.test(jobInfo.positionName?.replace(/\n/g, '') ?? '')
|
||||
}
|
||||
} catch {
|
||||
@@ -385,7 +403,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
|
||||
let isJobDescSuit = matchLogic === JobDetailRegExpMatchLogic.SOME ? false : true
|
||||
try {
|
||||
if (expectJobDescRegExpStr.trim()) {
|
||||
const regExp = new RegExp(expectJobDescRegExpStr, 'i')
|
||||
const regExp = new RegExp(expectJobDescRegExpStr, 'im')
|
||||
isJobDescSuit = regExp.test(jobInfo.postDescription?.replace(/\n/g, '') ?? '')
|
||||
}
|
||||
} catch {
|
||||
@@ -400,6 +418,7 @@ export function testIfJobTitleOrDescriptionSuit (jobInfo, matchLogic) {
|
||||
|
||||
async function setFilterCondition (selectedFilters) {
|
||||
const {
|
||||
cityList = [],
|
||||
salaryList = [],
|
||||
experienceList = [],
|
||||
degreeList = [],
|
||||
@@ -407,9 +426,9 @@ async function setFilterCondition (selectedFilters) {
|
||||
industryList = []
|
||||
} = selectedFilters
|
||||
|
||||
const placeholderTexts = ['薪资待遇', '工作经验', '学历要求', '公司行业', '公司规模']
|
||||
const optionKaPrefixes = ['sel-job-rec-salary-', 'sel-job-rec-exp-', 'sel-job-rec-degree-', 'sel-industry-', 'sel-job-rec-scale-']
|
||||
const conditionArr = [salaryList, experienceList, degreeList, industryList, scaleList]
|
||||
const placeholderTexts = ['城市', '薪资待遇', '工作经验', '学历要求', '公司行业', '公司规模']
|
||||
const optionKaPrefixes = ['switch_city_dialog_open', 'sel-job-rec-salary-', 'sel-job-rec-exp-', 'sel-job-rec-degree-', 'sel-industry-', 'sel-job-rec-scale-']
|
||||
const conditionArr = [cityList, salaryList, experienceList, degreeList, industryList, scaleList]
|
||||
|
||||
console.log('current filter condition----')
|
||||
for (let i = 0; i < placeholderTexts.length; i++) {
|
||||
@@ -428,10 +447,15 @@ async function setFilterCondition (selectedFilters) {
|
||||
const placeholderText = placeholderTexts[i]
|
||||
const filterDropdownProxy = await (async () => {
|
||||
const jsHandle = (await page.evaluateHandle((placeholderText) => {
|
||||
const filterBar = document.querySelector('.page-jobs-main .filter-condition-inner')
|
||||
const dropdownEntry = filterBar.__vue__.$children.find(it => it.placeholder === placeholderText)
|
||||
return dropdownEntry.$el
|
||||
}, placeholderText)).asElement();
|
||||
if (placeholderText === '城市') {
|
||||
return document.querySelector('.page-jobs-main .filter-condition-inner [ka="switch_city_dialog_open"]')
|
||||
}
|
||||
else {
|
||||
const filterBar = document.querySelector('.page-jobs-main .filter-condition-inner')
|
||||
const dropdownEntry = filterBar.__vue__.$children.find(it => it.placeholder === placeholderText)
|
||||
return dropdownEntry?.$el
|
||||
}
|
||||
}, placeholderText))?.asElement();
|
||||
return jsHandle
|
||||
})()
|
||||
if (!filterDropdownProxy) {
|
||||
@@ -440,88 +464,143 @@ async function setFilterCondition (selectedFilters) {
|
||||
|
||||
const currentFilterConditions = conditionArr[i];
|
||||
const filterDropdownCssList = await filterDropdownProxy.evaluate(el => Array.from(el.classList));
|
||||
if (!filterDropdownCssList.includes('is-select') && !currentFilterConditions.length) {
|
||||
continue
|
||||
} else {
|
||||
const filterDropdownElBBox = await filterDropdownProxy.boundingBox()
|
||||
await page.mouse.move(
|
||||
filterDropdownElBBox.x + filterDropdownElBBox.width / 2,
|
||||
filterDropdownElBBox.y + filterDropdownElBBox.height / 2,
|
||||
)
|
||||
await sleepWithRandomDelay(500)
|
||||
if (placeholderText === '城市') {
|
||||
const onPageSelectedCity = filterDropdownCssList.includes('active') ? (await filterDropdownProxy.evaluate(el => el.textContent.trim())) : null
|
||||
if (!onPageSelectedCity && !currentFilterConditions.length) {
|
||||
continue
|
||||
} else if (onPageSelectedCity === (currentFilterConditions[0] ?? null)) {
|
||||
continue
|
||||
} else {
|
||||
if (!currentFilterConditions.length) {
|
||||
const clearButtonHandle = await page.$(`.page-jobs-main .filter-condition-inner [ka="empty-filter"]`)
|
||||
await clearButtonHandle.click()
|
||||
}
|
||||
else {
|
||||
await filterDropdownProxy?.click()
|
||||
await page.waitForFunction(() => {
|
||||
const dialogEl = document.querySelector('.city-select-dialog')
|
||||
return dialogEl && window.getComputedStyle(dialogEl).display !== 'none'
|
||||
})
|
||||
const citySelectWrapperProxy = await page.waitForSelector('.city-select-wrapper')
|
||||
let targetCityElJsHandle = (await page.evaluateHandle((cityName) => {
|
||||
const targetCityEl = [...document.querySelectorAll('.city-select-dialog .city-select-wrapper ul.city-list-hot li')].find(it => it.textContent.trim() === cityName) ?? null
|
||||
return targetCityEl
|
||||
}, currentFilterConditions[0]))?.asElement()
|
||||
if (!targetCityElJsHandle) {
|
||||
const targetCityItem = flattedCityList.find(it => it.name === currentFilterConditions[0])
|
||||
if (!targetCityItem) {
|
||||
// unexpected condition
|
||||
continue
|
||||
}
|
||||
const firstChar = targetCityItem.firstChar
|
||||
const targetCityCharListEntryHandle = await page.$(`xpath///*[contains(@class, "city-select-dialog")]//*[contains(@class, "city-select-wrapper")]//ul[contains(@class, "city-char-list")]//li[contains(text(), '${firstChar.toUpperCase()}')]`)
|
||||
await targetCityCharListEntryHandle.click()
|
||||
targetCityElJsHandle = (await page.evaluateHandle((cityName) => {
|
||||
const targetCityEl = [...document.querySelectorAll('.city-select-dialog .city-select-wrapper .list-select-list a')].find(it => it.textContent.trim() === cityName) ?? null
|
||||
return targetCityEl
|
||||
}, currentFilterConditions[0]))?.asElement()
|
||||
}
|
||||
if (!targetCityElJsHandle) {
|
||||
// unexpected condition
|
||||
continue
|
||||
}
|
||||
await targetCityElJsHandle.click()
|
||||
await sleep(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!filterDropdownCssList.includes('is-select') && !currentFilterConditions.length) {
|
||||
continue
|
||||
} else {
|
||||
await filterDropdownProxy.scrollIntoView()
|
||||
const filterDropdownElBBox = await filterDropdownProxy.boundingBox()
|
||||
await page.mouse.move(
|
||||
filterDropdownElBBox.x + filterDropdownElBBox.width / 2,
|
||||
filterDropdownElBBox.y + filterDropdownElBBox.height / 2,
|
||||
)
|
||||
await sleepWithRandomDelay(500)
|
||||
|
||||
const optionKaPrefix = optionKaPrefixes[i]
|
||||
if (!currentFilterConditions.length) {
|
||||
if (placeholderText === '公司行业') {
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||||
for (const it of activeOptionElAtCurrentFilterProxyList) {
|
||||
await it.click()
|
||||
const optionKaPrefix = optionKaPrefixes[i]
|
||||
if (!currentFilterConditions.length) {
|
||||
if (placeholderText === '公司行业') {
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||||
for (const it of activeOptionElAtCurrentFilterProxyList) {
|
||||
await it.click()
|
||||
}
|
||||
} else {
|
||||
// select 不限 immediately
|
||||
const buxianOptionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${0}"]`)
|
||||
await buxianOptionElProxy.click()
|
||||
}
|
||||
} else {
|
||||
// select 不限 immediately
|
||||
const buxianOptionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${0}"]`)
|
||||
await buxianOptionElProxy.click()
|
||||
}
|
||||
} else {
|
||||
//#region uncheck options perviously checked but not existed in current filter.
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||||
const activeOptionValues = (await Promise.all(
|
||||
activeOptionElAtCurrentFilterProxyList.map(elProxy => {
|
||||
return elProxy.evaluate((el) => {
|
||||
return el.getAttribute('ka')
|
||||
//#region uncheck options perviously checked but not existed in current filter.
|
||||
const activeOptionElAtCurrentFilterProxyList = await page.$$(`.page-jobs-main .filter-condition-inner .active[ka^="${optionKaPrefix}"]`)
|
||||
const activeOptionValues = (await Promise.all(
|
||||
activeOptionElAtCurrentFilterProxyList.map(elProxy => {
|
||||
return elProxy.evaluate((el) => {
|
||||
return el.getAttribute('ka')
|
||||
})
|
||||
})
|
||||
})
|
||||
)).map(it => it.replace(optionKaPrefix, '')).map(Number)
|
||||
if (placeholderText !== '薪资待遇') {
|
||||
for(let i = 0; i < activeOptionValues.length; i++) {
|
||||
let activeValue
|
||||
)).map(it => it.replace(optionKaPrefix, '')).map(Number)
|
||||
if (placeholderText !== '薪资待遇') {
|
||||
for(let i = 0; i < activeOptionValues.length; i++) {
|
||||
let activeValue
|
||||
if (placeholderText === '公司行业') {
|
||||
activeValue = industryFilterConditionsMapByIndex[activeOptionValues[i]]?.code
|
||||
} else {
|
||||
activeValue = activeOptionValues[i]
|
||||
}
|
||||
const activeOptionElProxy = activeOptionElAtCurrentFilterProxyList[i]
|
||||
if (!currentFilterConditions.includes(activeValue)) {
|
||||
await activeOptionElProxy.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
//#region only click the one which we need check, don't change already checked.
|
||||
const conditionToCheck = currentFilterConditions.filter(it => {
|
||||
if (placeholderText === '公司行业') {
|
||||
activeValue = industryFilterConditionsMapByIndex[activeOptionValues[i]]?.code
|
||||
return !activeOptionValues.map(value => industryFilterConditionsMapByIndex[value].code).includes(it);
|
||||
} else {
|
||||
activeValue = activeOptionValues[i]
|
||||
return !activeOptionValues.includes(it)
|
||||
}
|
||||
const activeOptionElProxy = activeOptionElAtCurrentFilterProxyList[i]
|
||||
if (!currentFilterConditions.includes(activeValue)) {
|
||||
await activeOptionElProxy.click()
|
||||
})
|
||||
for(let j = 0; j < conditionToCheck.length; j++) {
|
||||
let optionValue
|
||||
if (placeholderText === '公司行业') {
|
||||
optionValue = industryFilterConditionCodeToIndexMap[conditionToCheck[j]]
|
||||
} else {
|
||||
optionValue = conditionToCheck[j]
|
||||
}
|
||||
await sleepWithRandomDelay(500)
|
||||
await filterDropdownProxy.scrollIntoView()
|
||||
const filterDropdownElBBox = await filterDropdownProxy.boundingBox()
|
||||
await page.mouse.move(
|
||||
filterDropdownElBBox.x + filterDropdownElBBox.width / 2,
|
||||
filterDropdownElBBox.y + filterDropdownElBBox.height / 2,
|
||||
)
|
||||
await sleepWithRandomDelay(500)
|
||||
const optionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${optionValue}"]`)
|
||||
if (!optionElProxy) {
|
||||
continue;
|
||||
}
|
||||
await optionElProxy.click()
|
||||
}
|
||||
//#endregion
|
||||
//#region move out dropdown entry to make dropdown hidden
|
||||
const navBarLogoElProxy = await page.$(`[ka="header-home-logo"]`)
|
||||
if (navBarLogoElProxy) {
|
||||
const navBarLogoElBBox = await navBarLogoElProxy.boundingBox()
|
||||
await page.mouse.move(
|
||||
navBarLogoElBBox.x + navBarLogoElBBox.width / 2,
|
||||
navBarLogoElBBox.y + navBarLogoElBBox.height / 2,
|
||||
)
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
//#endregion
|
||||
//#region only click the one which we need check, don't change already checked.
|
||||
const conditionToCheck = currentFilterConditions.filter(it => {
|
||||
if (placeholderText === '公司行业') {
|
||||
return !activeOptionValues.map(value => industryFilterConditionsMapByIndex[value].code).includes(it);
|
||||
} else {
|
||||
return !activeOptionValues.includes(it)
|
||||
}
|
||||
})
|
||||
for(let j = 0; j < conditionToCheck.length; j++) {
|
||||
let optionValue
|
||||
if (placeholderText === '公司行业') {
|
||||
optionValue = industryFilterConditionCodeToIndexMap[conditionToCheck[j]]
|
||||
} else {
|
||||
optionValue = conditionToCheck[j]
|
||||
}
|
||||
await sleepWithRandomDelay(500)
|
||||
const optionElProxy = await page.$(`.page-jobs-main .filter-condition-inner [ka="${optionKaPrefix}${optionValue}"]`)
|
||||
if (!optionElProxy) {
|
||||
continue;
|
||||
}
|
||||
await optionElProxy.click()
|
||||
}
|
||||
//#endregion
|
||||
//#region move out dropdown entry to make dropdown hidden
|
||||
const navBarLogoElProxy = await page.$(`[ka="header-home-logo"]`)
|
||||
if (navBarLogoElProxy) {
|
||||
const navBarLogoElBBox = await navBarLogoElProxy.boundingBox()
|
||||
await page.mouse.move(
|
||||
navBarLogoElBBox.x + navBarLogoElBBox.width / 2,
|
||||
navBarLogoElBBox.y + navBarLogoElBBox.height / 2,
|
||||
)
|
||||
}
|
||||
//#endregion
|
||||
await sleepWithRandomDelay(500)
|
||||
}
|
||||
await sleepWithRandomDelay(500)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -679,9 +758,12 @@ async function toRecommendPage (hooks) {
|
||||
? formatStaticCombineFilters(staticCombineRecommendJobFilterConditions)
|
||||
: combineFiltersWithConstraintsGenerator(anyCombineRecommendJobFilter)
|
||||
let expectJobList
|
||||
let filterConditionIndex = -1
|
||||
iterateFilterCondition: for (
|
||||
const filterCondition of filterConditions
|
||||
) {
|
||||
filterConditionIndex++
|
||||
console.log(`current filter condition index to apply: ${filterConditionIndex}`, JSON.stringify(filterCondition))
|
||||
findInCurrentFilterCondition: while(true) {
|
||||
await sleepWithRandomDelay(2500)
|
||||
|
||||
@@ -738,6 +820,10 @@ async function toRecommendPage (hooks) {
|
||||
);
|
||||
await storeStorage(page).catch(() => void 0)
|
||||
await sleepWithRandomDelay(2000)
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'afterJobSourceChosen',
|
||||
hooks
|
||||
})
|
||||
}
|
||||
await sleepWithRandomDelay(1500)
|
||||
await setFilterCondition(filterCondition)
|
||||
@@ -931,7 +1017,10 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
}
|
||||
requestNextPagePromiseWithResolver = null
|
||||
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'afterJobListPageFetched',
|
||||
hooks
|
||||
})
|
||||
await sleep(5000)
|
||||
await updateJobListData()
|
||||
tempTargetJobIndexToCheckDetail = getTempTargetJobIndexToCheckDetail()
|
||||
@@ -975,6 +1064,10 @@ async function toRecommendPage (hooks) {
|
||||
);
|
||||
await sleepWithRandomDelay(2000)
|
||||
}
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'afterJobDetailFetched',
|
||||
hooks
|
||||
})
|
||||
targetJobData = await page.evaluate('document.querySelector(".job-detail-box").__vue__.data')
|
||||
selectedJobData = await page.evaluate('document.querySelector(".page-jobs-main").__vue__.currentJob')
|
||||
// save the job detail info
|
||||
@@ -1002,6 +1095,10 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
else if (jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'beforeJobNotSuitMarked',
|
||||
hooks
|
||||
})
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.BOSS_INACTIVE)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
@@ -1040,6 +1137,10 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
else if (expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'beforeJobNotSuitMarked',
|
||||
hooks
|
||||
})
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
@@ -1077,6 +1178,10 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
else if (expectWorkExpNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'beforeJobNotSuitMarked',
|
||||
hooks
|
||||
})
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
@@ -1114,6 +1219,10 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
else if (jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'beforeJobNotSuitMarked',
|
||||
hooks
|
||||
})
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
@@ -1154,6 +1263,10 @@ async function toRecommendPage (hooks) {
|
||||
}
|
||||
else if (expectSalaryNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS) {
|
||||
try {
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'beforeJobNotSuitMarked',
|
||||
hooks
|
||||
})
|
||||
const { chosenReasonInUi } = await markJobAsNotSuitInRecommendPage(MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT)
|
||||
await hooks.jobMarkedAsNotSuit.promise(
|
||||
targetJobData,
|
||||
@@ -1271,6 +1384,10 @@ async function toRecommendPage (hooks) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'beforeJobChatStartup',
|
||||
hooks
|
||||
})
|
||||
await sleepWithRandomDelay(1000)
|
||||
const startChatButtonInnerHTML = await page.evaluate('document.querySelector(".job-detail-box .op-btn.op-btn-chat")?.innerHTML.trim()')
|
||||
|
||||
@@ -1319,6 +1436,10 @@ async function toRecommendPage (hooks) {
|
||||
res.zpData.bizData?.chatRemindDialog?.blockLevel === 0 &&
|
||||
/剩\d+次沟通机会/.test(res.zpData.bizData?.chatRemindDialog?.content)
|
||||
) {
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'beforeJobChatStartupAfterTwiceConfirm',
|
||||
hooks
|
||||
})
|
||||
const confirmButton = await page.waitForSelector('.chat-block-dialog .chat-block-footer .sure-btn')
|
||||
await confirmButton.click()
|
||||
const nextRes = await waitAddFriendResponse()
|
||||
@@ -1328,6 +1449,10 @@ async function toRecommendPage (hooks) {
|
||||
res.zpData.bizCode === 1 &&
|
||||
/猎头/.test(res.zpData.bizData?.chatRemindDialog?.content)
|
||||
) {
|
||||
await waitForSageTimeOrJustContinue({
|
||||
tag: 'beforeJobChatStartupAfterTwiceConfirm',
|
||||
hooks
|
||||
})
|
||||
const confirmButton = await page.waitForSelector(`xpath///*[contains(@class, "chat-block-dialog")]//*[contains(@class, "chat-block-footer")]//*[contains(text(), "继续")]`)
|
||||
await confirmButton.click()
|
||||
const nextRes = await waitAddFriendResponse()
|
||||
@@ -1428,6 +1553,7 @@ export async function mainLoop (hooks) {
|
||||
}
|
||||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
|
||||
await page.bringToFront()
|
||||
// __GGR_INJECT_ANTI_ANTI_DEBUGGER__
|
||||
await hooks.mainFlowWillLaunch?.promise({
|
||||
jobNotMatchStrategy,
|
||||
jobNotActiveStrategy,
|
||||
|
||||
54
packages/geek-auto-start-chat-with-boss/sage-time.mjs
Normal file
54
packages/geek-auto-start-chat-with-boss/sage-time.mjs
Normal file
@@ -0,0 +1,54 @@
|
||||
import { readConfigFile } from './runtime-file-utils.mjs'
|
||||
import { sleep } from "@geekgeekrun/utils/sleep.mjs"
|
||||
let {
|
||||
isSageTimeEnabled,
|
||||
sageTimeOpTimes,
|
||||
sageTimePauseMinute,
|
||||
} = readConfigFile('boss.json')
|
||||
isSageTimeEnabled = isSageTimeEnabled ?? true
|
||||
sageTimeOpTimes =
|
||||
isNaN(parseInt(sageTimeOpTimes)) ||
|
||||
parseInt(sageTimeOpTimes) < 1
|
||||
? 100
|
||||
: parseInt(sageTimeOpTimes)
|
||||
sageTimePauseMinute =
|
||||
isNaN(parseFloat(sageTimePauseMinute)) ||
|
||||
parseFloat(sageTimePauseMinute) < 0
|
||||
? 15
|
||||
: parseFloat(sageTimePauseMinute)
|
||||
if (parseFloat(sageTimePauseMinute) === 0) {
|
||||
isSageTimeEnabled = false
|
||||
}
|
||||
|
||||
let totalEnabledTimes = 0
|
||||
let recordedOpCount = 0
|
||||
export const waitForSageTimeOrJustContinue = async ({
|
||||
tag,
|
||||
hooks,
|
||||
} = {}) => {
|
||||
if (!isSageTimeEnabled) {
|
||||
return
|
||||
}
|
||||
const tagText = tag ? ` ${tag}` : ''
|
||||
if (recordedOpCount > sageTimeOpTimes) {
|
||||
totalEnabledTimes++
|
||||
console.log(`[SageTime${tagText}] 请求已达限制,开启;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次开启`)
|
||||
await hooks?.sageTimeEnter?.promise({
|
||||
tag,
|
||||
totalEnabledTimes,
|
||||
recordedOpCount,
|
||||
})
|
||||
await sleep(sageTimePauseMinute * 60 * 1000)
|
||||
console.log(`[SageTime${tagText}] 请求限制已解除,关闭;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次关闭`)
|
||||
await hooks?.sageTimeExit?.promise({
|
||||
tag,
|
||||
totalEnabledTimes,
|
||||
recordedOpCount,
|
||||
})
|
||||
recordedOpCount = 0
|
||||
}
|
||||
else {
|
||||
console.log(`[SageTime${tagText}] 请求未达限制;当前记录次数 ${recordedOpCount};已开启过 ${totalEnabledTimes} 次`)
|
||||
recordedOpCount++
|
||||
}
|
||||
}
|
||||
1
packages/laodeng/index.js
Normal file
1
packages/laodeng/index.js
Normal file
File diff suppressed because one or more lines are too long
27
packages/laodeng/package.json
Normal file
27
packages/laodeng/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@geekgeekrun/puppeteer-extra-plugin-laodeng",
|
||||
"version": "0.0.1",
|
||||
"description": "laodeng",
|
||||
"main": "./index.js",
|
||||
"author": "geekgeekrun",
|
||||
"productName": "GeekGeekRun",
|
||||
"homepage": "https://github.com/geekgeekrun",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin": "^3.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright-extra": "*",
|
||||
"puppeteer": "^24.19.0",
|
||||
"puppeteer-extra": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"puppeteer-extra": {
|
||||
"optional": true
|
||||
},
|
||||
"playwright-extra": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,8 @@ export async function main() {
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: [
|
||||
`--load-extension=${editThisCookieExtensionPath}`
|
||||
]
|
||||
pipe: true,
|
||||
enableExtensions: [editThisCookieExtensionPath]
|
||||
})
|
||||
|
||||
const closeAttachedSet = new WeakSet()
|
||||
|
||||
@@ -57,7 +57,9 @@ const main = async () => {
|
||||
noPositionFoundForCurrentJob: new SyncHook(),
|
||||
noPositionFoundAfterTraverseAllJob: new SyncHook(),
|
||||
errorEncounter: new SyncHook(['errorInfo']),
|
||||
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args'])
|
||||
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']),
|
||||
sageTimeEnter: new AsyncSeriesHook(['args']),
|
||||
sageTimeExit: new AsyncSeriesHook(['args'])
|
||||
}
|
||||
initPlugins(hooks)
|
||||
await hooks.daemonInitialized.callAsync()
|
||||
|
||||
15
packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts
Normal file
15
packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { JobHireStatus } from "../enums";
|
||||
import { requireTypeorm } from "../utils/module-loader";
|
||||
const { Entity, Column, PrimaryColumn } = requireTypeorm();
|
||||
|
||||
@Entity()
|
||||
export class JobHireStatusRecord {
|
||||
@PrimaryColumn()
|
||||
encryptJobId: string;
|
||||
|
||||
@Column()
|
||||
hireStatus: JobHireStatus;
|
||||
|
||||
@Column()
|
||||
lastSeenDate: Date;
|
||||
}
|
||||
@@ -38,4 +38,10 @@ export enum JobSource {
|
||||
export enum CombineRecommendJobFilterType {
|
||||
ANY_COMBINE = 1,
|
||||
STATIC_COMBINE = 2,
|
||||
}
|
||||
|
||||
export enum JobHireStatus {
|
||||
HIRING = 1,
|
||||
CLOSED = 2,
|
||||
DELETED = 3,
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
|
||||
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
|
||||
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
|
||||
import { LlmModelUsageRecord } from "./entity/LlmModelUsageRecord";
|
||||
import { JobHireStatusRecord } from "./entity/JobHireStatusRecord";
|
||||
|
||||
function getBossInfoIfIsEqual (savedOne, currentOne) {
|
||||
if (savedOne === currentOne) {
|
||||
@@ -365,3 +366,25 @@ export async function getBossIdsByJobIds (ds: DataSource, jobIds: string[] = [])
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export async function saveJobHireStatusRecord(
|
||||
ds: DataSource,
|
||||
record: JobHireStatusRecord
|
||||
) {
|
||||
const jobHireStatusRecordRepository = ds.getRepository(JobHireStatusRecord);
|
||||
await jobHireStatusRecordRepository.save(record);
|
||||
return
|
||||
}
|
||||
|
||||
export async function getJobHireStatusRecord(
|
||||
ds: DataSource,
|
||||
encryptJobId: string
|
||||
) {
|
||||
const repo = ds.getRepository(JobHireStatusRecord)
|
||||
const result = await repo.findOne({
|
||||
where: {
|
||||
encryptJobId
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { VCompanyLibrary } from "./entity/VCompanyLibrary"
|
||||
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
|
||||
import { ChatMessageRecord } from './entity/ChatMessageRecord'
|
||||
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
|
||||
import { JobHireStatusRecord } from './entity/JobHireStatusRecord'
|
||||
|
||||
import sqlite3 from 'sqlite3';
|
||||
import {
|
||||
@@ -28,15 +29,17 @@ import {
|
||||
saveMarkAsNotSuitRecord,
|
||||
getNotSuitMarkRecordsInLastSomeDays,
|
||||
getChatStartupRecordsInLastSomeDays,
|
||||
getBossIdsByJobIds
|
||||
getBossIdsByJobIds,
|
||||
saveJobHireStatusRecord
|
||||
} from "./handlers";
|
||||
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
|
||||
import minimist from 'minimist'
|
||||
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
|
||||
import { MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
|
||||
import { JobHireStatus, MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
|
||||
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
|
||||
import { Init1000000000000 } from "./migrations/1000000000000-Init";
|
||||
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
|
||||
import { AddJobHireStatusTable1766466476822 } from "./migrations/1766466476822-AddJobHireStatusTable";
|
||||
const lodashImportPromise = import('lodash-es')
|
||||
|
||||
export function initDb(dbFilePath) {
|
||||
@@ -67,13 +70,15 @@ export function initDb(dbFilePath) {
|
||||
VMarkAsNotSuitLog,
|
||||
ChatMessageRecord,
|
||||
LlmModelUsageRecord,
|
||||
JobHireStatusRecord,
|
||||
],
|
||||
migrations: [
|
||||
Init1000000000000,
|
||||
UpdateChatStartupLogTable1729182577167,
|
||||
UpdateBossInfoTable1732032381304,
|
||||
AddColumnForMarkAsNotSuitLog1746092370665,
|
||||
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526
|
||||
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526,
|
||||
AddJobHireStatusTable1766466476822
|
||||
],
|
||||
migrationsRun: true
|
||||
});
|
||||
@@ -197,6 +202,15 @@ export default class SqlitePlugin {
|
||||
await saveJobInfoFromRecommendPage(ds, _jobInfo);
|
||||
});
|
||||
|
||||
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async ({ jobInfo }) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveJobHireStatusRecord(ds, {
|
||||
encryptJobId: jobInfo.encryptId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList, jobSource = undefined } = {}) => {
|
||||
const ds = await this.initPromise;
|
||||
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
export class AddJobHireStatusTable1766466476822 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE IF NOT EXISTS "job_hire_status_record" ("encryptJobId" varchar PRIMARY KEY NOT NULL, "hireStatus" integer NOT NULL, "lastSeenDate" datetime NOT NULL);`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "geekgeekrun-ui",
|
||||
"version": "0.10.2",
|
||||
"version": "0.14.0",
|
||||
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "geekgeekrun",
|
||||
@@ -33,6 +33,7 @@
|
||||
"@geekgeekrun/dingtalk-plugin": "workspace:*",
|
||||
"@geekgeekrun/geek-auto-start-chat-with-boss": "workspace:*",
|
||||
"@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension": "workspace:*",
|
||||
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
|
||||
"@geekgeekrun/sqlite-plugin": "workspace:*",
|
||||
"@geekgeekrun/utils": "workspace:*",
|
||||
"JSONStream": "^1.3.5",
|
||||
@@ -43,7 +44,7 @@
|
||||
"mitt": "^3.0.1",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"pinia": "^3.0.2",
|
||||
"puppeteer": "20.1.0",
|
||||
"puppeteer": "24.19.0",
|
||||
"puppeteer-extra-plugin-stealth": "2.11.2",
|
||||
"uuid": "^11.1.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
@@ -54,7 +55,7 @@
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@inquirer/prompts": "^7.0.1",
|
||||
"@inquirer/select": "^4.0.1",
|
||||
"@puppeteer/browsers": "^2.0.0",
|
||||
"@puppeteer/browsers": "^2.11.0",
|
||||
"@rushstack/eslint-patch": "^1.7.1",
|
||||
"@types/node": "^18.19.9",
|
||||
"@unocss/preset-attributify": "^0.58.5",
|
||||
@@ -71,7 +72,7 @@
|
||||
"element-plus": "^2.8.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"find-chrome-bin": "^2.0.1",
|
||||
"find-chrome-bin": "^2.0.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"prettier": "^3.2.4",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 109 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "0.10.2",
|
||||
"buildVersion": 24,
|
||||
"buildTime": 1765548632185,
|
||||
"buildHash": "056ec7a0a6c3225829dff3da36712fb38ab7635f",
|
||||
"version": "0.14.0",
|
||||
"buildVersion": 30,
|
||||
"buildTime": 1769326019101,
|
||||
"buildHash": "962dcb1125db93faff6159b618fb9384c4156e76",
|
||||
"name": "geekgeekrun-ui"
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export enum DOWNLOAD_ERROR_EXIT_CODE {
|
||||
|
||||
export const checkAndDownloadDependenciesForInit = async () => {
|
||||
process.on('disconnect', () => app.exit())
|
||||
process.on('uncaughtException', () => app.exit(DOWNLOAD_ERROR_EXIT_CODE.DOWNLOAD_ERROR))
|
||||
app.dock?.hide()
|
||||
let pipe: null | fs.WriteStream = null
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface BrowserInfo {
|
||||
executablePath: string
|
||||
}
|
||||
|
||||
const CONFIG_VSERION = 2
|
||||
|
||||
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
|
||||
export const lastUsedBrowserRecordFilePath = path.join(
|
||||
runtimeFolderPath,
|
||||
@@ -28,8 +30,13 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
|
||||
}
|
||||
try {
|
||||
const fileContent = (await fsPromise.readFile(lastUsedBrowserRecordFilePath)).toString()
|
||||
const [path, browser] = fileContent.split('\n').map((it) => it.trim())
|
||||
if (!path || !fs.existsSync(path)) {
|
||||
const [path, browser, configVersion] = fileContent.split('\n').map((it) => it.trim())
|
||||
if (
|
||||
!path ||
|
||||
!fs.existsSync(path) ||
|
||||
!Number(configVersion) ||
|
||||
Number(configVersion) < CONFIG_VSERION
|
||||
) {
|
||||
await removeLastUsedAndAvailableBrowserPath()
|
||||
return null
|
||||
}
|
||||
@@ -57,7 +64,7 @@ export const saveLastUsedAndAvailableBrowserInfo = async (browserInfo: BrowserIn
|
||||
}
|
||||
await fsPromise.writeFile(
|
||||
lastUsedBrowserRecordFilePath,
|
||||
[browserInfo.executablePath, browserInfo.browser].join('\n')
|
||||
[browserInfo.executablePath, browserInfo.browser, CONFIG_VSERION].join('\n')
|
||||
)
|
||||
} catch {
|
||||
console.warn('lastUsedBrowserRecordFile write error')
|
||||
|
||||
@@ -16,7 +16,7 @@ const getPuppeteerManagerModule = async () => {
|
||||
return puppeteerManager
|
||||
}
|
||||
|
||||
const EXPECT_CHROMIUM_BUILD_ID = '113.0.5672.63'
|
||||
const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
|
||||
const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')
|
||||
|
||||
const getExpectCachedPuppeteerExecutable = async (): Promise<BrowserInfo> => {
|
||||
@@ -130,7 +130,7 @@ export async function findAndLocateUserInstalledChromiumExecutableSync(): Promis
|
||||
const findChrome: typeof import('find-chrome-bin').findChrome = (await import('find-chrome-bin'))
|
||||
.findChrome
|
||||
const targetBrowser = await findChrome({
|
||||
min: exceptChromiumMainVersion
|
||||
min: exceptChromiumMainVersion + 1
|
||||
})
|
||||
if (!targetBrowser?.executablePath) {
|
||||
throw new Error('NO_EXPECT_CHROMIUM_FOUND')
|
||||
|
||||
@@ -94,7 +94,9 @@ const runAutoChat = async () => {
|
||||
noPositionFoundForCurrentJob: new SyncHook(),
|
||||
noPositionFoundAfterTraverseAllJob: new SyncHook(),
|
||||
errorEncounter: new SyncHook(['errorInfo']),
|
||||
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args'])
|
||||
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']),
|
||||
sageTimeEnter: new AsyncSeriesHook(['args']),
|
||||
sageTimeExit: new AsyncSeriesHook(['args'])
|
||||
}
|
||||
initPlugins(hooks)
|
||||
|
||||
|
||||
@@ -6,18 +6,24 @@ import {
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import {
|
||||
RECOMMEND_JOB_ENTRY_SELECTOR,
|
||||
USER_SET_EXPECT_JOB_ENTRIES_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,
|
||||
saveChatStartupRecord,
|
||||
saveMarkAsNotSuitRecord,
|
||||
saveChatMessageRecord
|
||||
saveChatMessageRecord,
|
||||
saveJobHireStatusRecord
|
||||
} 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, JobSource } from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import {
|
||||
MarkAsNotSuitReason,
|
||||
JobSource,
|
||||
JobHireStatus
|
||||
} from '@geekgeekrun/sqlite-plugin/dist/enums'
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import { Target } from 'puppeteer'
|
||||
@@ -42,6 +48,74 @@ const attachRequestsListener = async (target: Target) => {
|
||||
if (!page) {
|
||||
return
|
||||
}
|
||||
|
||||
function handleJobDetailPage({ encryptJobId } = { encryptJobId: null }) {
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
if (!encryptJobId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
({ encryptJobId }) => {
|
||||
return (
|
||||
location.href.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
|
||||
document.readyState === 'complete'
|
||||
)
|
||||
},
|
||||
undefined,
|
||||
{ encryptJobId }
|
||||
)
|
||||
const htmlContent = await page.content()
|
||||
if (htmlContent) {
|
||||
const $ = cheerio.load(htmlContent)
|
||||
const [jobBannerEl] = $('#main .job-banner') ?? []
|
||||
if (!jobBannerEl) {
|
||||
console.log(`access might be blocked`)
|
||||
if (
|
||||
htmlContent.includes(`您访问的页面不存在`) ||
|
||||
location.href === `https://www.zhipin.com/`
|
||||
) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.DELETED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
|
||||
if (jobStatusTextEl) {
|
||||
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
|
||||
if ([`职位已关闭`].includes(jobStatusText)) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.CLOSED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
} else {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
})
|
||||
.catch(() => void 0)
|
||||
}
|
||||
|
||||
if (page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
|
||||
const encryptJobId = page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
|
||||
if (encryptJobId) {
|
||||
handleJobDetailPage({ encryptJobId })
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentJobSource() {
|
||||
const methodMap = {
|
||||
async recommend() {
|
||||
@@ -87,12 +161,24 @@ const attachRequestsListener = async (target: Target) => {
|
||||
}
|
||||
|
||||
page.on('response', async (response) => {
|
||||
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
|
||||
if (response.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
|
||||
const encryptJobId = response
|
||||
.url()
|
||||
.match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
|
||||
if (encryptJobId) {
|
||||
handleJobDetailPage({ encryptJobId })
|
||||
}
|
||||
} else if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
|
||||
const data = await response.json()
|
||||
|
||||
console.log(data)
|
||||
if (data.code === 0) {
|
||||
await saveJobInfoFromRecommendPage(await dbInitPromise, data.zpData)
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId: data.zpData.jobInfo.encryptId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/reasons.json')
|
||||
@@ -243,11 +329,15 @@ const attachRequestsListener = async (target: Target) => {
|
||||
)
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const chatRecordList = rawChatRecordList.map(it => {
|
||||
const chatRecordList = rawChatRecordList.map((it) => {
|
||||
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
|
||||
mappedItem.mid = it.mid
|
||||
mappedItem.encryptFromUserId = it.isSelf ? currentUserInfo.encryptUserId : bossInfo.encryptBossId
|
||||
mappedItem.encryptToUserId = it.isSelf ? bossInfo.encryptBossId : currentUserInfo.encryptUserId
|
||||
mappedItem.encryptFromUserId = it.isSelf
|
||||
? currentUserInfo.encryptUserId
|
||||
: bossInfo.encryptBossId
|
||||
mappedItem.encryptToUserId = it.isSelf
|
||||
? bossInfo.encryptBossId
|
||||
: currentUserInfo.encryptUserId
|
||||
mappedItem.style = it.isSelf ? 'sent' : 'received'
|
||||
mappedItem.type = it.type
|
||||
mappedItem.time = it.time ? new Date(it.time) : null
|
||||
@@ -281,7 +371,8 @@ export async function launchBossSite() {
|
||||
const { puppeteer } = await initPuppeteer()
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: [`--load-extension=${editThisCookieExtensionPath}`]
|
||||
pipe: true,
|
||||
enableExtensions: [editThisCookieExtensionPath]
|
||||
})
|
||||
let [page] = await browser.pages()
|
||||
for (let i = 0; i < bossCookies.length; i++) {
|
||||
|
||||
@@ -166,6 +166,15 @@ export default function initIpc() {
|
||||
bossConfig.staticCombineRecommendJobFilterConditions =
|
||||
payload.staticCombineRecommendJobFilterConditions
|
||||
}
|
||||
if (hasOwn(payload, 'isSageTimeEnabled')) {
|
||||
bossConfig.isSageTimeEnabled = payload.isSageTimeEnabled
|
||||
}
|
||||
if (hasOwn(payload, 'sageTimeOpTimes')) {
|
||||
bossConfig.sageTimeOpTimes = payload.sageTimeOpTimes
|
||||
}
|
||||
if (hasOwn(payload, 'sageTimePauseMinute')) {
|
||||
bossConfig.sageTimePauseMinute = payload.sageTimePauseMinute
|
||||
}
|
||||
|
||||
promiseArr.push(writeConfigFile('boss.json', bossConfig))
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ export async function launchBoss(browser: Browser) {
|
||||
}
|
||||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
|
||||
try {
|
||||
await Promise.all([page.goto(bossChatUiUrl, { timeout: 0 }), page.waitForNavigation()])
|
||||
await Promise.all([
|
||||
page.goto(bossChatUiUrl, { timeout: 0 }),
|
||||
page.waitForNavigation({ timeout: 120 * 1000 })
|
||||
])
|
||||
} catch (error) {
|
||||
if (error?.message?.startsWith('net::ERR_INTERNET_DISCONNECTED')) {
|
||||
throw new Error('ERR_INTERNET_DISCONNECTED')
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MsgStatus, type ChatListItem } from './types'
|
||||
import { Browser, Page } from 'puppeteer'
|
||||
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
|
||||
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
|
||||
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
|
||||
import { app, dialog } from 'electron'
|
||||
import { initDb } from '@geekgeekrun/sqlite-plugin'
|
||||
@@ -11,7 +12,11 @@ import {
|
||||
readConfigFile
|
||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import { ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatMessageRecord'
|
||||
import { saveChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
import {
|
||||
saveChatMessageRecord,
|
||||
getJobHireStatusRecord,
|
||||
saveJobHireStatusRecord
|
||||
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
|
||||
import { writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||
import * as fs from 'fs'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
@@ -19,6 +24,9 @@ import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
|
||||
import { messageForSaveFilter } from '../../../common/utils/chat-list'
|
||||
import { RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat'
|
||||
import gtag from '../../utils/gtag'
|
||||
import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums';
|
||||
import dayjs from 'dayjs'
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
const throttleIntervalMinutes =
|
||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||
@@ -104,6 +112,88 @@ async function saveCurrentChatRecord(page) {
|
||||
await saveChatMessageRecord(ds, chatRecordList)
|
||||
}
|
||||
|
||||
async function checkJobIsClosed() {
|
||||
const encryptJobId = await pageMapByName.boss!.evaluate(() => {
|
||||
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$.encryptJobId
|
||||
})
|
||||
if (!encryptJobId) {
|
||||
return false
|
||||
}
|
||||
let isJobClosed = false
|
||||
let record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
|
||||
// not seen before, or last seen more than 6 hours ago
|
||||
// fetch new status
|
||||
if (
|
||||
!record ||
|
||||
(record.hireStatus === JobHireStatus.HIRING &&
|
||||
Date.now() - Number(dayjs(record.lastSeenDate)) > 6 * 60 * 60 * 1000)
|
||||
) {
|
||||
const positionNameElHandle = await pageMapByName.boss!.$(
|
||||
`#main .chat-conversation [ka="geek_chat_job_detail"] .right-content`
|
||||
)
|
||||
if (!positionNameElHandle) {
|
||||
return false
|
||||
}
|
||||
positionNameElHandle.click()
|
||||
try {
|
||||
const targetPage = await waitForPage(pageMapByName.boss!.browser(), async (page) => {
|
||||
const url = page.url()
|
||||
if (
|
||||
url.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
|
||||
(await page.evaluate(() => document.readyState === 'complete'))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
const htmlContent = await targetPage.content()
|
||||
if (htmlContent) {
|
||||
const $ = cheerio.load(htmlContent)
|
||||
const [jobBannerEl] = $('#main .job-banner') ?? []
|
||||
if (!jobBannerEl) {
|
||||
console.log(`access might be blocked`)
|
||||
if (
|
||||
htmlContent.includes(`您访问的页面不存在`) ||
|
||||
location.href === `https://www.zhipin.com/`
|
||||
) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.DELETED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
|
||||
if (jobStatusTextEl) {
|
||||
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
|
||||
if ([`职位已关闭`].includes(jobStatusText)) {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.CLOSED,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
} else {
|
||||
await saveJobHireStatusRecord(await dbInitPromise, {
|
||||
encryptJobId,
|
||||
hireStatus: JobHireStatus.HIRING,
|
||||
lastSeenDate: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
targetPage.close().catch(() => void 0)
|
||||
} catch (err) {
|
||||
console.log(`checkJobIsClosed error: ${err}`)
|
||||
}
|
||||
record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
|
||||
}
|
||||
if (record.hireStatus !== JobHireStatus.HIRING) {
|
||||
isJobClosed = true
|
||||
}
|
||||
return isJobClosed
|
||||
}
|
||||
|
||||
let browser: null | Browser = null
|
||||
const mainLoop = async () => {
|
||||
if (browser) {
|
||||
@@ -201,7 +291,9 @@ const mainLoop = async () => {
|
||||
(rechatLimitDay && it.updateTime
|
||||
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
|
||||
: true) &&
|
||||
((((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) ||
|
||||
((((it.lastIsSelf &&
|
||||
it.lastMsgStatus === MsgStatus.HAS_READ &&
|
||||
!it.lastText.includes('你撤回了')) ||
|
||||
canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) &&
|
||||
!it.unreadCount) ||
|
||||
(!it.lastIsSelf && it.lastText === '开场问题,期待你的回答'))
|
||||
@@ -301,7 +393,9 @@ const mainLoop = async () => {
|
||||
)?.filter(messageForSaveFilter) ?? []
|
||||
|
||||
const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0
|
||||
const isJobClosed = await checkJobIsClosed()
|
||||
if (
|
||||
!isJobClosed &&
|
||||
isExpectJobTypeMatch &&
|
||||
historyMessageList[historyMessageList.length - 1].isSelf &&
|
||||
historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ &&
|
||||
|
||||
@@ -31,5 +31,11 @@ export default class GtagPlugin {
|
||||
hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => {
|
||||
gtag('encounter_empty_rec_job_list', { pageQuery })
|
||||
})
|
||||
hooks.sageTimeEnter.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
|
||||
gtag('sage_time_enter', { tag, totalEnabledTimes, recordedOpCount })
|
||||
})
|
||||
hooks.sageTimeExit.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
|
||||
gtag('sage_time_exit', { tag, totalEnabledTimes, recordedOpCount })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
<template>
|
||||
<div class="job-combo-filter">
|
||||
<div class="filter-item">
|
||||
<div font-size-12px>城市</div>
|
||||
<div
|
||||
style="
|
||||
align-items: center;
|
||||
background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
background-image: none;
|
||||
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
|
||||
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
|
||||
"
|
||||
pl4px
|
||||
pr4px
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
>
|
||||
<city-chooser
|
||||
v-model="modelValue.cityList"
|
||||
gt-show-scene="any-combine-boss-recommend-filter"
|
||||
>
|
||||
<template #default="{ showDialog }">
|
||||
<div flex justify-between items-center>
|
||||
<div font-size-12px>
|
||||
<template v-if="modelValue.cityList?.length"
|
||||
>已选择<span ml3px mr3px>{{ modelValue.cityList?.length }}</span
|
||||
>个城市</template
|
||||
>
|
||||
<template v-else><i color-gray>未选择城市</i></template>
|
||||
</div>
|
||||
<el-button size="small" @click="showDialog" pl4px pr4px>选择</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</city-chooser>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<div font-size-12px>薪资待遇</div>
|
||||
<el-select
|
||||
@@ -10,7 +45,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.salaryList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.salaryList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -27,7 +62,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.experienceList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.experienceList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -44,7 +79,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.degreeList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.degreeList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -84,7 +119,7 @@
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-option
|
||||
v-for="it in conditions.scaleList.filter(it => it.code !== 0)"
|
||||
v-for="it in conditions.scaleList.filter((it) => it.code !== 0)"
|
||||
:key="it.code"
|
||||
:value="it.code"
|
||||
:label="it.name"
|
||||
@@ -97,6 +132,7 @@
|
||||
<script lang="ts" setup>
|
||||
import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-conditions-20241002.json'
|
||||
import industryFilterExemption from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||||
import CityChooser from '@renderer/page/MainLayout/GeekAutoStartChatWithBoss/components/CityChooser.vue'
|
||||
import { PropType } from 'vue'
|
||||
|
||||
defineProps({
|
||||
|
||||
@@ -71,6 +71,25 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="城市" prop="city">
|
||||
<template #default="{ row }">
|
||||
<city-chooser
|
||||
v-model="row.city"
|
||||
:multiple="false"
|
||||
gt-show-scene="static-combine-boss-recommend-filter"
|
||||
>
|
||||
<template #default="{ showDialog }">
|
||||
<div flex justify-between items-center>
|
||||
<div font-size-12px lh-1.2em>
|
||||
<template v-if="row.city">{{ row.city }}</template>
|
||||
<template v-else><i color-gray>未选择城市</i></template>
|
||||
</div>
|
||||
<el-button size="small" pl4px pr4px @click="showDialog">选择</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</city-chooser>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :resizable="false" label="薪资待遇" prop="salary">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
@@ -183,6 +202,7 @@ import conditions from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-con
|
||||
import industryFilterExemption from '@geekgeekrun/geek-auto-start-chat-with-boss/internal-config/job-filter-industry-filter-exemption-20241002.json'
|
||||
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { computed, PropType } from 'vue'
|
||||
import CityChooser from '@renderer/page/MainLayout/GeekAutoStartChatWithBoss/components/CityChooser.vue'
|
||||
|
||||
import { getStaticCombineFilterKey } from '@geekgeekrun/geek-auto-start-chat-with-boss/combineCalculator.mjs'
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onUnmounted, PropType } from 'vue'
|
||||
import { ref, onUnmounted, PropType, h } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import FailMessage from './FailMessage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
dependenciesStatus: {
|
||||
@@ -36,12 +38,24 @@ const downloadProcessExitCode = ref(0)
|
||||
const processDownloadBrowser = async () => {
|
||||
downloadProcessExitCode.value = 0
|
||||
browserDownloadPercentage.value = 0
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('setup-dependencies')
|
||||
browserDownloadPercentage.value = 100
|
||||
} catch (err) {
|
||||
downloadProcessExitCode.value = 1
|
||||
throw err
|
||||
let restRetriedTime = 2
|
||||
while (restRetriedTime > 0) {
|
||||
try {
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('setup-dependencies')
|
||||
browserDownloadPercentage.value = 100
|
||||
} catch (err) {
|
||||
downloadProcessExitCode.value = 1
|
||||
throw err
|
||||
}
|
||||
break
|
||||
} catch (err) {
|
||||
restRetriedTime--
|
||||
if (restRetriedTime === 0) {
|
||||
throw err
|
||||
}
|
||||
await sleep(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +82,13 @@ const processTasks = async () => {
|
||||
await p
|
||||
} catch {
|
||||
gtagRenderer('encounter_error_when_download_deps')
|
||||
await ElMessageBox.confirm('需要重试吗?', '核心组件下载失败', {
|
||||
await ElMessageBox.confirm(h(FailMessage), {
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
showClose: false,
|
||||
type: 'error',
|
||||
cancelButtonText: '退出程序'
|
||||
cancelButtonText: '退出程序',
|
||||
confirmButtonText: '重试'
|
||||
})
|
||||
.then(() => {
|
||||
gtagRenderer('start_retry_download_deps')
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>核心组件下载失败,请重试。</p>
|
||||
<br />
|
||||
<br />
|
||||
<p>
|
||||
<b text-orange>提示:</b>由于网络颠簸,如果多次重试仍然失败,请 <el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
font-size-14px
|
||||
@click="handleOpenChromeDownloadPage"
|
||||
>点击此处</el-button
|
||||
> 下载最新版本 Google Chrome
|
||||
浏览器,安装完毕后,重新打开本程序,程序会自动检测该浏览器并使用它。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import debounce from 'lodash-es/debounce'
|
||||
const { ipcRenderer } = electron
|
||||
|
||||
const handleOpenChromeDownloadPage = debounce(
|
||||
async () => {
|
||||
gtagRenderer('open_chrome_download_page_clicked')
|
||||
ipcRenderer.send('open-external-link', 'https://www.google.cn/chrome/')
|
||||
},
|
||||
1000,
|
||||
{ leading: true, trailing: false }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,56 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="modelValue?.length">
|
||||
<div>当前已选择城市:</div>
|
||||
<div flex flex-wrap gap-10px>
|
||||
<el-tag v-for="it in modelValue" :key="it">
|
||||
{{ it }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>当前未选择任何期望城市,将不会按照城市进行筛选</div>
|
||||
</div>
|
||||
<div
|
||||
line-height-1
|
||||
:style="{
|
||||
marginTop: modelValue?.length ? '10px' : ''
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="
|
||||
() => {
|
||||
isDialogVisible = true
|
||||
gtagRenderer('choose_city_entry_button_clicked')
|
||||
}
|
||||
"
|
||||
>选择城市</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="modelValue?.length"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleClearSelectedCitiesInModelValue"
|
||||
>清空已选择的所有城市</el-button
|
||||
>
|
||||
</div>
|
||||
<div w-full>
|
||||
<slot
|
||||
:model-value="modelValue"
|
||||
:show-dialog="() => (isDialogVisible = true)"
|
||||
:clear-value="handleClearSelectedCitiesInModelValue"
|
||||
></slot>
|
||||
<el-dialog
|
||||
v-model="isDialogVisible"
|
||||
width="1000px"
|
||||
title="请选择城市"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
@open="handleDialogOpen"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<el-tabs v-model="activeTabName">
|
||||
<el-tab-pane
|
||||
:style="{ height: '300px', overflow: 'auto' }"
|
||||
:style="{ height: '260px', overflow: 'auto' }"
|
||||
label="热门城市"
|
||||
name="热门城市"
|
||||
>
|
||||
<el-checkbox-group v-model="selectedCities">
|
||||
<el-checkbox-group v-if="multiple" v-model="selectedCities">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
@@ -66,6 +36,23 @@
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<el-radio-group v-else v-model="selectedCities" w-full>
|
||||
<div
|
||||
w-full
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-radio
|
||||
v-for="op in hotCityList.filter((it) => it.code !== 100010000)"
|
||||
:key="op.code"
|
||||
:label="op.name"
|
||||
>
|
||||
{{ op.name }}
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-for="it in cityGroupsByAlphabetMap.keys()"
|
||||
@@ -75,8 +62,8 @@
|
||||
:value="it"
|
||||
>
|
||||
<div v-for="group in cityGroupsByAlphabetMap.get(it)" :key="group.firstChar">
|
||||
{{ group.firstChar }}
|
||||
<el-checkbox-group v-model="selectedCities">
|
||||
<div pt4px pb4px>{{ group.firstChar }}</div>
|
||||
<el-checkbox-group v-if="multiple" v-model="selectedCities">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
@@ -88,6 +75,19 @@
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<el-radio-group v-else v-model="selectedCities" w-full>
|
||||
<div
|
||||
w-full
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-radio v-for="op in group.cityList" :key="op.code" :label="op.name">
|
||||
{{ op.name }}
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -95,19 +95,66 @@
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
alignItems: multiple ? 'end' : 'center',
|
||||
justifyContent: 'space-between'
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="selectedCities.length"
|
||||
type="danger"
|
||||
@click="handleClearSelectedCitiesInDialog"
|
||||
>清空已选择的所有城市</el-button
|
||||
>
|
||||
<div flex flex-1 mr12px text-left flex-col>
|
||||
<template v-if="selectedCities?.length">
|
||||
<div
|
||||
flex
|
||||
flex-items-center
|
||||
font-size-14px
|
||||
flex-0
|
||||
ws-nowrap
|
||||
:class="{ mb10px: multiple }"
|
||||
>
|
||||
<el-button
|
||||
v-if="multiple && selectedCities?.length"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleClearSelectedCitiesInDialog"
|
||||
>清空已选择的所有城市</el-button
|
||||
>
|
||||
<template v-if="!multiple">
|
||||
<span ml6px font-size-13px class="color-#999">已选择:</span>
|
||||
<el-tag
|
||||
closable
|
||||
@close="
|
||||
() => {
|
||||
selectedCities = null
|
||||
gtagRenderer('remove_selected_cities_in_dialog_clicked', {
|
||||
gtShowScene: props.gtShowScene,
|
||||
multiple: Boolean(multiple)
|
||||
})
|
||||
}
|
||||
"
|
||||
>{{ selectedCities }}</el-tag
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="multiple" flex flex-1 flex-wrap gap-6px of-auto max-h-160px>
|
||||
<span font-size-13px class="color-#999" flex items-center>已选择:</span>
|
||||
<el-tag
|
||||
v-for="(city, index) in selectedCities"
|
||||
:key="city"
|
||||
closable
|
||||
@close="
|
||||
() => {
|
||||
;(selectedCities ?? []).splice(index, 1)
|
||||
gtagRenderer('remove_selected_cities_in_dialog_clicked', {
|
||||
gtShowScene: props.gtShowScene,
|
||||
multiple: Boolean(multiple)
|
||||
})
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ city }}</el-tag
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<div flex-0 ws-nowrap>
|
||||
<el-button @click="handleCancelClicked">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmClicked">确定</el-button>
|
||||
</div>
|
||||
@@ -119,12 +166,21 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, ref } from 'vue'
|
||||
import cityGroupData from '../../../../../../common/constant/cityGroup.json'
|
||||
import cityGroupData from '@geekgeekrun/geek-auto-start-chat-with-boss/cityGroup.mjs'
|
||||
import { gtagRenderer } from '@renderer/utils/gtag'
|
||||
import { ElRadioGroup } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<string[]>
|
||||
type: [Array, String] as PropType<string[] | string | null>,
|
||||
default: null
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
gtShowScene: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
@@ -132,7 +188,7 @@ const { hotCityList, cityGroup } = cityGroupData.zpData
|
||||
|
||||
const activeTabName = ref('热门城市')
|
||||
const isDialogVisible = ref(false)
|
||||
const selectedCities = ref([])
|
||||
const selectedCities = ref(null)
|
||||
|
||||
const cityGroupsByAlphabetMap = ref(
|
||||
new Map(['ABCDE', 'FGHJ', 'KLMN', 'PQRST', 'WXYZ'].map((it) => [it, []]))
|
||||
@@ -151,30 +207,38 @@ for (const group of cityGroup) {
|
||||
|
||||
function handleDialogOpen() {
|
||||
activeTabName.value = '热门城市'
|
||||
selectedCities.value = [...(props.modelValue ?? [])]
|
||||
gtagRenderer('choose_city_dialog_open')
|
||||
selectedCities.value = props.multiple ? [...(props.modelValue ?? [])] : props.modelValue
|
||||
gtagRenderer('choose_city_dialog_open', { gtShowScene: props.gtShowScene })
|
||||
}
|
||||
|
||||
function handleCancelClicked() {
|
||||
gtagRenderer('choose_city_cancel_button_clicked')
|
||||
gtagRenderer('choose_city_cancel_button_clicked', { gtShowScene: props.gtShowScene })
|
||||
isDialogVisible.value = false
|
||||
}
|
||||
function handleConfirmClicked() {
|
||||
gtagRenderer('choose_city_confirm_button_clicked', { value: selectedCities.value.join(',') })
|
||||
gtagRenderer('choose_city_confirm_button_clicked', {
|
||||
gtShowScene: props.gtShowScene,
|
||||
value: Array.isArray(selectedCities.value)
|
||||
? selectedCities.value.join(',')
|
||||
: selectedCities.value
|
||||
})
|
||||
isDialogVisible.value = false
|
||||
emits('update:modelValue', [...(selectedCities.value ?? [])])
|
||||
emits(
|
||||
'update:modelValue',
|
||||
props.multiple ? [...(selectedCities.value ?? [])] : selectedCities.value
|
||||
)
|
||||
}
|
||||
function handleDialogClosed() {
|
||||
selectedCities.value = []
|
||||
gtagRenderer('choose_city_dialog_closed')
|
||||
selectedCities.value = props.multiple ? [] : null
|
||||
gtagRenderer('choose_city_dialog_closed', { gtShowScene: props.gtShowScene })
|
||||
}
|
||||
|
||||
function handleClearSelectedCitiesInModelValue() {
|
||||
emits('update:modelValue', [])
|
||||
gtagRenderer('clear_selected_cities_in_mv_clicked')
|
||||
emits('update:modelValue', (selectedCities.value = props.multiple ? [] : null))
|
||||
gtagRenderer('clear_selected_cities_in_mv_clicked', { gtShowScene: props.gtShowScene })
|
||||
}
|
||||
function handleClearSelectedCitiesInDialog() {
|
||||
selectedCities.value = []
|
||||
gtagRenderer('clear_selected_cities_in_dialog_clicked')
|
||||
selectedCities.value = props.multiple ? [] : null
|
||||
gtagRenderer('clear_selected_cities_in_dialog_clicked', { gtShowScene: props.gtShowScene })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="form-wrap geek-auto-start-run-with-boss">
|
||||
<el-form ref="formRef" :model="formContent" label-position="top" :rules="formRules">
|
||||
<el-card class="config-section">
|
||||
<el-form-item mb0>
|
||||
<el-form-item>
|
||||
<div>
|
||||
<div font-size-16px>BOSS直聘 Cookie</div>
|
||||
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
|
||||
@@ -16,6 +16,100 @@
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<div font-size-16px>
|
||||
摸鱼模式
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
placement="bottom-start"
|
||||
@show="gtagRenderer('tooltip_show_about_sage_t')"
|
||||
>
|
||||
<template #content>
|
||||
<div>
|
||||
本程序运行较长时间后,Boss直聘会对账号进行风控,导致本程序不能继续执行。<br />
|
||||
为此,加入摸鱼模式。通过此配置,主动减慢本程序的运行速度,降低潜在的被风控监测到的概率。<br />
|
||||
你可以自定义开启摸鱼模式的频率 - 默认为操作100次后,暂停运行15分钟。
|
||||
</div>
|
||||
</template>
|
||||
<el-button type="text" font-size-12px
|
||||
><span><QuestionFilled w-1em h-1em mr2px /></span
|
||||
>这个配置会对本程序运行过程造成什么影响?</el-button
|
||||
>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-checkbox
|
||||
v-model="formContent.isSageTimeEnabled"
|
||||
@change="
|
||||
(v) => {
|
||||
gtagRenderer('sage_t_enable_changed', { v })
|
||||
}
|
||||
"
|
||||
>
|
||||
启用摸鱼模式
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div pl-1.5em font-size-14px>
|
||||
<div
|
||||
:style="{
|
||||
color: formContent.isSageTimeEnabled ? '' : '#aaa'
|
||||
}"
|
||||
>
|
||||
当如下行为的次数总计达
|
||||
<el-form-item mb0 inline-block prop="sageTimeOpTimes">
|
||||
<el-input-number
|
||||
v-model="formContent.sageTimeOpTimes"
|
||||
:step="1"
|
||||
step-strictly
|
||||
:precision="0"
|
||||
:min="1"
|
||||
:disabled="!formContent.isSageTimeEnabled"
|
||||
controls-position="right"
|
||||
@change="
|
||||
(v) => {
|
||||
gtagRenderer('sage_t_op_times_changed', { v })
|
||||
}
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
次时,暂停运行
|
||||
<el-form-item mb0 inline-block prop="sageTimePauseMinute">
|
||||
<el-input-number
|
||||
v-model="formContent.sageTimePauseMinute"
|
||||
:step="0.5"
|
||||
step-strictly
|
||||
:precision="1"
|
||||
:min="0"
|
||||
:disabled="!formContent.isSageTimeEnabled"
|
||||
controls-position="right"
|
||||
@change="
|
||||
(v) => {
|
||||
gtagRenderer('sage_t_pause_minute_changed', { v })
|
||||
}
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
分钟:
|
||||
<ul
|
||||
pl-1em
|
||||
mb0
|
||||
mt14px
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
lineHeight: '1.5em'
|
||||
}"
|
||||
>
|
||||
<li>职位来源变更</li>
|
||||
<li>职位列表滚动后发生加载</li>
|
||||
<li>职位详情加载</li>
|
||||
<li>职位被标记不合适</li>
|
||||
<li>职位被开聊</li>
|
||||
</ul>
|
||||
<div mt14px>之后继续运行并重新计次,循环整个过程</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="config-section">
|
||||
<el-form-item class="job-source-form-item" prop="__jobSourceList">
|
||||
@@ -220,7 +314,47 @@
|
||||
width: '100%'
|
||||
}"
|
||||
>
|
||||
<city-chooser v-model="formContent.expectCityList" />
|
||||
<city-chooser v-model="formContent.expectCityList">
|
||||
<template #default="{ modelValue, showDialog, clearValue }">
|
||||
<div v-if="modelValue?.length">
|
||||
<div>当前已选择城市:</div>
|
||||
<div flex flex-wrap gap-10px>
|
||||
<el-tag v-for="it in modelValue" :key="it">
|
||||
{{ it }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>当前未选择任何期望城市,将不会按照城市进行筛选</div>
|
||||
</div>
|
||||
<div
|
||||
line-height-1
|
||||
:style="{
|
||||
marginTop: modelValue?.length ? '10px' : ''
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="
|
||||
() => {
|
||||
// isDialogVisible = true
|
||||
showDialog()
|
||||
gtagRenderer('choose_city_entry_button_clicked')
|
||||
}
|
||||
"
|
||||
>选择城市</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="modelValue?.length"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="clearValue"
|
||||
>清空已选择的所有城市</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</city-chooser>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div
|
||||
@@ -1035,7 +1169,10 @@ const formContent = ref({
|
||||
type: 'expect',
|
||||
enabled: true
|
||||
}
|
||||
])
|
||||
]),
|
||||
isSageTimeEnabled: true,
|
||||
sageTimeOpTimes: 100,
|
||||
sageTimePauseMinute: 15
|
||||
})
|
||||
|
||||
const anyCombineBossRecommendFilterHasCondition = computed(() => {
|
||||
@@ -1182,6 +1319,17 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
|
||||
formContent.value.__jobSourceList = formatJobSourceConfigToFormValue(
|
||||
res.config['boss.json'].jobSourceList || []
|
||||
)
|
||||
formContent.value.isSageTimeEnabled = res.config['boss.json'].isSageTimeEnabled ?? true
|
||||
formContent.value.sageTimeOpTimes =
|
||||
isNaN(parseInt(res.config['boss.json'].sageTimeOpTimes)) ||
|
||||
parseInt(res.config['boss.json'].sageTimeOpTimes) < 1
|
||||
? 100
|
||||
: parseInt(res.config['boss.json'].sageTimeOpTimes)
|
||||
formContent.value.sageTimePauseMinute =
|
||||
isNaN(parseFloat(res.config['boss.json'].sageTimePauseMinute)) ||
|
||||
parseFloat(res.config['boss.json'].sageTimePauseMinute) < 0
|
||||
? 15
|
||||
: parseFloat(res.config['boss.json'].sageTimePauseMinute)
|
||||
})
|
||||
|
||||
const jobSourceFormItemSectionEl = ref()
|
||||
@@ -1274,6 +1422,32 @@ const formRules = {
|
||||
}
|
||||
cb()
|
||||
}
|
||||
},
|
||||
sageTimeOpTimes: {
|
||||
validator(_, value, cb) {
|
||||
if (!formContent.value.isSageTimeEnabled) {
|
||||
cb()
|
||||
return
|
||||
}
|
||||
if (value < 1 || isNaN(parseInt(value))) {
|
||||
cb(new Error(`最小值为1,请重试`))
|
||||
return
|
||||
}
|
||||
cb()
|
||||
}
|
||||
},
|
||||
sageTimePauseMinute: {
|
||||
validator(_, value, cb) {
|
||||
if (!formContent.value.isSageTimeEnabled) {
|
||||
cb()
|
||||
return
|
||||
}
|
||||
if (value < 0 || isNaN(parseFloat(value))) {
|
||||
cb(new Error(`最小值为0,请重试`))
|
||||
return
|
||||
}
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1285,7 +1459,12 @@ const handleSubmit = async () => {
|
||||
expect_job_type_reg_exp_str: formContent.value?.expectJobTypeRegExpStr,
|
||||
expect_job_desc_reg_exp_str: formContent.value?.expectJobDescRegExpStr,
|
||||
crjf_type: formContent.value?.combineRecommendJobFilterType,
|
||||
crjf_cc: currentAnyCombineRecommendJobFilterCombinationCount.value?.toLocaleString?.()
|
||||
crjf_cc: currentAnyCombineRecommendJobFilterCombinationCount.value?.toLocaleString?.(),
|
||||
sage_t_config: JSON.stringify({
|
||||
isEnabled: formContent.value.isSageTimeEnabled,
|
||||
pauseMinute: formContent.value.sageTimePauseMinute,
|
||||
opTimes: formContent.value.sageTimeOpTimes
|
||||
})
|
||||
})
|
||||
// remove the obsolete filter - expectJobRegExpStr
|
||||
formContent.value.expectJobRegExpStr = undefined
|
||||
@@ -1316,7 +1495,17 @@ const handleSubmit = async () => {
|
||||
}
|
||||
const handleSave = async () => {
|
||||
gtagRenderer('save_config_clicked', {
|
||||
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
|
||||
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken,
|
||||
expect_job_name_reg_exp_str: formContent.value?.expectJobNameRegExpStr,
|
||||
expect_job_type_reg_exp_str: formContent.value?.expectJobTypeRegExpStr,
|
||||
expect_job_desc_reg_exp_str: formContent.value?.expectJobDescRegExpStr,
|
||||
crjf_type: formContent.value?.combineRecommendJobFilterType,
|
||||
crjf_cc: currentAnyCombineRecommendJobFilterCombinationCount.value?.toLocaleString?.(),
|
||||
sage_t_config: JSON.stringify({
|
||||
isEnabled: formContent.value.isSageTimeEnabled,
|
||||
pauseMinute: formContent.value.sageTimePauseMinute,
|
||||
opTimes: formContent.value.sageTimeOpTimes
|
||||
})
|
||||
})
|
||||
normalizeExpectCompanies()
|
||||
try {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 793 KiB After Width: | Height: | Size: 1.2 MiB |
32
packages/utils/puppeteer/wait.mjs
Normal file
32
packages/utils/puppeteer/wait.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
export async function waitForPage(browser, predicate, {
|
||||
polling = 1000, timeout = 10 * 1000
|
||||
} = {}) {
|
||||
let intervalTimer = null
|
||||
let timeoutTimer = null
|
||||
return await new Promise((resolve, reject) => {
|
||||
timeoutTimer = setTimeout(
|
||||
() => {
|
||||
clearInterval(intervalTimer)
|
||||
clearInterval(timeoutTimer)
|
||||
reject(
|
||||
new Error(`waitForPage timeout after ${timeout} ms`)
|
||||
)
|
||||
},
|
||||
timeout
|
||||
)
|
||||
intervalTimer = setInterval(async () => {
|
||||
const allPages = await browser.pages()
|
||||
let target = null
|
||||
for (let i = allPages.length - 1; i >= 0; i--) {
|
||||
const currentPage = allPages[i]
|
||||
if (
|
||||
await predicate(currentPage)
|
||||
) {
|
||||
clearInterval(intervalTimer)
|
||||
clearInterval(timeoutTimer)
|
||||
resolve(currentPage)
|
||||
}
|
||||
}
|
||||
}, polling)
|
||||
})
|
||||
}
|
||||
23
patches/find-chrome-bin@2.0.4.patch
Normal file
23
patches/find-chrome-bin@2.0.4.patch
Normal file
@@ -0,0 +1,23 @@
|
||||
diff --git a/src/win32/index.js b/src/win32/index.js
|
||||
index 53e2279e4a0c51dfe0725100005fafed2c6537a5..d9de7840e2ad0bfe625e6067cc764cee220035f1 100644
|
||||
--- a/src/win32/index.js
|
||||
+++ b/src/win32/index.js
|
||||
@@ -15,12 +15,13 @@ export function findChromeBinaryOnWin32(canary) {
|
||||
].filter(Boolean)
|
||||
|
||||
let result
|
||||
-
|
||||
- prefixes.forEach(prefix => {
|
||||
+ for (const prefix of prefixes) {
|
||||
let chromePath = join(prefix, suffix)
|
||||
- if (canAccess(chromePath)) result = chromePath
|
||||
- })
|
||||
-
|
||||
+ if (canAccess(chromePath)) {
|
||||
+ result = chromePath
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
return result
|
||||
}
|
||||
|
||||
745
pnpm-lock.yaml
generated
745
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user