diff --git a/package.json b/package.json index 90dff91..cba91f4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts b/packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts new file mode 100644 index 0000000..d2f1aaf --- /dev/null +++ b/packages/sqlite-plugin/src/entity/JobHireStatusRecord.ts @@ -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; +} \ No newline at end of file diff --git a/packages/sqlite-plugin/src/enums.ts b/packages/sqlite-plugin/src/enums.ts index d70ab1e..c2ad8e1 100644 --- a/packages/sqlite-plugin/src/enums.ts +++ b/packages/sqlite-plugin/src/enums.ts @@ -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, } \ No newline at end of file diff --git a/packages/sqlite-plugin/src/handlers.ts b/packages/sqlite-plugin/src/handlers.ts index 61bc121..7e06675 100644 --- a/packages/sqlite-plugin/src/handlers.ts +++ b/packages/sqlite-plugin/src/handlers.ts @@ -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 +} \ No newline at end of file diff --git a/packages/sqlite-plugin/src/index.ts b/packages/sqlite-plugin/src/index.ts index 3722f87..cfb0e92 100644 --- a/packages/sqlite-plugin/src/index.ts +++ b/packages/sqlite-plugin/src/index.ts @@ -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, { diff --git a/packages/sqlite-plugin/src/migrations/1766466476822-AddJobHireStatusTable.ts b/packages/sqlite-plugin/src/migrations/1766466476822-AddJobHireStatusTable.ts new file mode 100644 index 0000000..7fcf654 --- /dev/null +++ b/packages/sqlite-plugin/src/migrations/1766466476822-AddJobHireStatusTable.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +export class AddJobHireStatusTable1766466476822 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + } + +} diff --git a/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts b/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts index eed4c86..0559584 100644 --- a/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts +++ b/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts @@ -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 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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86f5fa2..57d1cf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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