add job hire status record table and relevant save logic

This commit is contained in:
geekgeekrun
2025-12-23 17:05:21 +08:00
parent e986fddbfb
commit aa410cfc60
8 changed files with 254 additions and 12 deletions

View File

@@ -11,6 +11,7 @@
"author": "geekgeekrun",
"license": "ISC",
"dependencies": {
"cheerio": "1.0.0-rc.12",
"dayjs": "^1.11.10",
"json5": "^2.2.3",
"minimist": "^1.2.8",

View 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;
}

View File

@@ -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,
}

View File

@@ -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,12 @@ 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
}

View File

@@ -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, {

View File

@@ -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> {
}
}

View File

@@ -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

98
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
cheerio:
specifier: 1.0.0-rc.12
version: 1.0.0-rc.12
dayjs:
specifier: ^1.11.10
version: 1.11.10
@@ -2641,7 +2644,6 @@ packages:
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: true
/boolean@3.2.0:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
@@ -2812,6 +2814,30 @@ packages:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true
/cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
dependencies:
boolbase: 1.0.0
css-select: 5.2.2
css-what: 6.2.2
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
dev: false
/cheerio@1.0.0-rc.12:
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
engines: {node: '>= 6'}
dependencies:
cheerio-select: 2.1.0
dom-serializer: 2.0.0
domhandler: 5.0.3
domutils: 3.2.2
htmlparser2: 8.0.2
parse5: 7.3.0
parse5-htmlparser2-tree-adapter: 7.1.0
dev: false
/chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -3052,6 +3078,16 @@ packages:
which: 2.0.2
dev: true
/css-select@5.2.2:
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
dependencies:
boolbase: 1.0.0
css-what: 6.2.2
domhandler: 5.0.3
domutils: 3.2.2
nth-check: 2.1.1
dev: false
/css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@@ -3060,6 +3096,11 @@ packages:
source-map-js: 1.0.2
dev: true
/css-what@6.2.2:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
dev: false
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -3251,6 +3292,33 @@ packages:
esutils: 2.0.3
dev: true
/dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
dev: false
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false
/domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: false
/domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: false
/dotenv-expand@5.1.0:
resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==}
dev: true
@@ -3417,6 +3485,11 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
/entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
dev: false
/env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -4162,6 +4235,15 @@ packages:
lru-cache: 6.0.0
dev: true
/htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 4.5.0
dev: false
/http-cache-semantics@4.1.1:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
@@ -5046,7 +5128,6 @@ packages:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
@@ -5193,6 +5274,13 @@ packages:
parse5: 6.0.1
dev: false
/parse5-htmlparser2-tree-adapter@7.1.0:
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
dependencies:
domhandler: 5.0.3
parse5: 7.3.0
dev: false
/parse5@5.1.1:
resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==}
dev: false
@@ -5201,6 +5289,12 @@ packages:
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
dev: false
/parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
dependencies:
entities: 6.0.1
dev: false
/path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
dev: true