mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-08 00:47:15 +08:00
Merge pull request #1 from rqi14/feature/login
Feature/login 招聘端自动化 bug 修复 — 已测试,待合并
This commit is contained in:
@@ -31,7 +31,9 @@ import {
|
|||||||
CHAT_PAGE_NAME_SELECTOR,
|
CHAT_PAGE_NAME_SELECTOR,
|
||||||
CHAT_PAGE_JOB_SELECTOR,
|
CHAT_PAGE_JOB_SELECTOR,
|
||||||
CHAT_PAGE_PREVIEW_RESUME_BTN_SELECTOR,
|
CHAT_PAGE_PREVIEW_RESUME_BTN_SELECTOR,
|
||||||
CHAT_PAGE_ONLINE_RESUME_CLOSE_SELECTOR
|
CHAT_PAGE_ONLINE_RESUME_CLOSE_SELECTOR,
|
||||||
|
CHAT_PAGE_JOB_DROPDOWN_SELECTOR,
|
||||||
|
CHAT_PAGE_JOB_ITEM_SELECTOR
|
||||||
} from './constant.mjs'
|
} from './constant.mjs'
|
||||||
|
|
||||||
const LOG = '[chat-page-processor]'
|
const LOG = '[chat-page-processor]'
|
||||||
@@ -45,7 +47,7 @@ async function switchChatPageJobId (page, jobId) {
|
|||||||
try {
|
try {
|
||||||
const cursor = await createHumanCursor(page)
|
const cursor = await createHumanCursor(page)
|
||||||
// 用拟人轨迹点击下拉触发按钮
|
// 用拟人轨迹点击下拉触发按钮
|
||||||
const dropdownBtn = await page.$('.ui-dropmenu.chat-top-job .ui-dropmenu-label')
|
const dropdownBtn = await page.$(CHAT_PAGE_JOB_DROPDOWN_SELECTOR)
|
||||||
if (dropdownBtn) {
|
if (dropdownBtn) {
|
||||||
const box = await dropdownBtn.boundingBox().catch(() => null)
|
const box = await dropdownBtn.boundingBox().catch(() => null)
|
||||||
if (box) {
|
if (box) {
|
||||||
@@ -54,12 +56,12 @@ async function switchChatPageJobId (page, jobId) {
|
|||||||
await dropdownBtn.click()
|
await dropdownBtn.click()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await page.click('.ui-dropmenu.chat-top-job .ui-dropmenu-label')
|
await page.click(CHAT_PAGE_JOB_DROPDOWN_SELECTOR)
|
||||||
}
|
}
|
||||||
await page.waitForSelector('.ui-dropmenu.chat-top-job .ui-dropmenu-list', { timeout: 5000 })
|
await page.waitForSelector(CHAT_PAGE_JOB_ITEM_SELECTOR, { timeout: 5000 })
|
||||||
await sleepWithRandomDelay(150, 300)
|
await sleepWithRandomDelay(150, 300)
|
||||||
// 用拟人轨迹点击目标职位项
|
// 用拟人轨迹点击目标职位项
|
||||||
const items = await page.$$('.ui-dropmenu.chat-top-job .ui-dropmenu-list li')
|
const items = await page.$$(CHAT_PAGE_JOB_ITEM_SELECTOR)
|
||||||
let found = false
|
let found = false
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const val = await item.evaluate(el => el.getAttribute('value')).catch(() => null)
|
const val = await item.evaluate(el => el.getAttribute('value')).catch(() => null)
|
||||||
@@ -798,31 +800,52 @@ export default async function startBossChatPageProcess (hooksFromCaller, options
|
|||||||
// 「新招呼」分类与「未读」tab 已在上方初始化阶段完成切换(force: true),此处直接解析列表。
|
// 「新招呼」分类与「未读」tab 已在上方初始化阶段完成切换(force: true),此处直接解析列表。
|
||||||
// 若经过 retryCandidate 流程,retry 结束时已切回「未读」tab,状态同样正确。
|
// 若经过 retryCandidate 流程,retry 结束时已切回「未读」tab,状态同样正确。
|
||||||
|
|
||||||
const conversations = await parseConversationList(page)
|
// ── 批次循环:每处理 BATCH_REFRESH_SIZE 条后重新点击「未读」刷新列表(步骤4-5)────
|
||||||
logDebug(`${LOG} DOM 解析到 ${conversations.length} 条会话`)
|
const BATCH_REFRESH_SIZE = 10
|
||||||
|
let totalAttempted = 0
|
||||||
|
let totalProcessed = 0
|
||||||
|
const seenIds = new Set()
|
||||||
|
|
||||||
const unreadItems = conversations.filter((c) => c.encryptGeekId)
|
while (totalAttempted < maxProcessPerRun) {
|
||||||
const toProcess = unreadItems.slice(0, maxProcessPerRun)
|
const conversations = await parseConversationList(page)
|
||||||
logInfo(`${LOG} 未读会话 ${unreadItems.length} 条,本次最多处理 ${toProcess.length} 条`)
|
logDebug(`${LOG} DOM 解析到 ${conversations.length} 条会话`)
|
||||||
if (toProcess.length > 0) {
|
|
||||||
logDebug(`${LOG} 候选人列表:${toProcess.map((c, i) => `[${i}] ${c.geekName}(${c.encryptGeekId})`).join(', ')}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
await hooks.onProgress?.promise?.({ phase: 'chatPage', current: 0, max: toProcess.length }).catch(() => {})
|
const unreadItems = conversations.filter((c) => c.encryptGeekId && !seenIds.has(c.encryptGeekId))
|
||||||
|
if (unreadItems.length === 0) {
|
||||||
let processed = 0
|
logInfo(`${LOG} 「未读」列表为空,全部处理完毕`)
|
||||||
|
break
|
||||||
for (let i = 0; i < toProcess.length; i++) {
|
|
||||||
const item = toProcess[i]
|
|
||||||
logInfo(`${LOG} ── [${i + 1}/${toProcess.length}] 开始处理 ${item.geekName}(${item.encryptGeekId})──`)
|
|
||||||
const result = await processOneCandidateConversation(item)
|
|
||||||
if (result.processed) {
|
|
||||||
processed++
|
|
||||||
await hooks.onProgress?.promise?.({ phase: 'chatPage', current: processed, max: toProcess.length }).catch(() => {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const batchSize = Math.min(BATCH_REFRESH_SIZE, maxProcessPerRun - totalAttempted)
|
||||||
|
const batch = unreadItems.slice(0, batchSize)
|
||||||
|
logInfo(`${LOG} 当前未读 ${unreadItems.length} 条,本批次处理 ${batch.length} 条(已尝试 ${totalAttempted}/${maxProcessPerRun})`)
|
||||||
|
|
||||||
|
await hooks.onProgress?.promise?.({ phase: 'chatPage', current: totalProcessed, max: maxProcessPerRun }).catch(() => {})
|
||||||
|
|
||||||
|
for (const item of batch) {
|
||||||
|
seenIds.add(item.encryptGeekId)
|
||||||
|
logInfo(`${LOG} ── [${totalAttempted + 1}/${maxProcessPerRun}] 开始处理 ${item.geekName}(${item.encryptGeekId})──`)
|
||||||
|
const result = await processOneCandidateConversation(item)
|
||||||
|
totalAttempted++
|
||||||
|
if (result.processed) {
|
||||||
|
totalProcessed++
|
||||||
|
await hooks.onProgress?.promise?.({ phase: 'chatPage', current: totalProcessed, max: maxProcessPerRun }).catch(() => {})
|
||||||
|
}
|
||||||
|
if (totalAttempted >= maxProcessPerRun) break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalAttempted >= maxProcessPerRun) {
|
||||||
|
logInfo(`${LOG} 已达本次最大处理数 ${maxProcessPerRun},停止`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤4:每批次结束后重新点击「未读」标签,刷新列表
|
||||||
|
logInfo(`${LOG} 本批次结束,重新点击「未读」标签刷新列表...`)
|
||||||
|
await switchToTab(CHAT_PAGE_UNREAD_FILTER_SELECTOR, '未读', { force: true })
|
||||||
|
await sleepWithRandomDelay(400, 600)
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`${LOG} 本次共处理 ${processed} 条未读会话`)
|
logInfo(`${LOG} 本次共处理 ${totalProcessed} 条未读会话(尝试 ${totalAttempted} 条)`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await hooks.onError?.promise?.(err)
|
await hooks.onError?.promise?.(err)
|
||||||
throw err
|
throw err
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const RECOMMEND_JOB_LIST_SELECTOR = '#headerWrap ul.job-list'
|
|||||||
export const RECOMMEND_JOB_ITEM_SELECTOR = '#headerWrap ul.job-list li.job-item'
|
export const RECOMMEND_JOB_ITEM_SELECTOR = '#headerWrap ul.job-list li.job-item'
|
||||||
|
|
||||||
/** 沟通页:顶部职位筛选下拉触发按钮(点击展开职位列表) */
|
/** 沟通页:顶部职位筛选下拉触发按钮(点击展开职位列表) */
|
||||||
export const CHAT_PAGE_JOB_DROPDOWN_SELECTOR = '.chat-top-job .ui-dropmenu-label'
|
export const CHAT_PAGE_JOB_DROPDOWN_SELECTOR = '.dropmenu-label.chat-select-job'
|
||||||
/** 沟通页:职位下拉展开后的列表项(过滤 value="-1" 的"全部职位") */
|
/** 沟通页:职位下拉展开后的列表项(过滤 value="-1" 的"全部职位") */
|
||||||
export const CHAT_PAGE_JOB_ITEM_SELECTOR = '.chat-top-job .ui-dropmenu-list li'
|
export const CHAT_PAGE_JOB_ITEM_SELECTOR = '.chat-top-job .ui-dropmenu-list li'
|
||||||
|
|
||||||
@@ -239,4 +239,4 @@ export const GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR = '.dialog-uninstall-
|
|||||||
* 每次开始处理前须先点击此 tab,确保只扫描新招呼消息,避免遍历其他类型会话。
|
* 每次开始处理前须先点击此 tab,确保只扫描新招呼消息,避免遍历其他类型会话。
|
||||||
* HTML: div.chat-label-item[title="新招呼"],选中态有 class selected。
|
* HTML: div.chat-label-item[title="新招呼"],选中态有 class selected。
|
||||||
*/
|
*/
|
||||||
export const CHAT_PAGE_TAB_NEW_GREET_SELECTOR = '.chat-label-item[title="新招呼"]'
|
export const CHAT_PAGE_TAB_NEW_GREET_SELECTOR = '.chat-label-item[title^="新招呼"]'
|
||||||
|
|||||||
@@ -67,9 +67,11 @@ export async function initPuppeteer () {
|
|||||||
* 该弹窗在每次登录后必现,不处理会导致后续自动化操作卡死超时。
|
* 该弹窗在每次登录后必现,不处理会导致后续自动化操作卡死超时。
|
||||||
* @param {import('puppeteer').Page} page
|
* @param {import('puppeteer').Page} page
|
||||||
*/
|
*/
|
||||||
async function dismissGovernanceNoticeDialog (page) {
|
export async function dismissGovernanceNoticeDialog (page) {
|
||||||
try {
|
try {
|
||||||
const confirmBtn = await page.$(GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR)
|
const confirmBtn = await page
|
||||||
|
.waitForSelector(GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR, { timeout: 10000 })
|
||||||
|
.catch(() => null)
|
||||||
if (!confirmBtn) return
|
if (!confirmBtn) return
|
||||||
logInfo('[boss-auto-browse] 检测到「治理公告」弹窗,点击「我已知晓」关闭...')
|
logInfo('[boss-auto-browse] 检测到「治理公告」弹窗,点击「我已知晓」关闭...')
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat'
|
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat'
|
||||||
import { daemonEE, sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
|
import { daemonEE, sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||||
import { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db'
|
import { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db'
|
||||||
@@ -43,7 +44,7 @@ export async function runCommon({ mode }) {
|
|||||||
}
|
}
|
||||||
const args =
|
const args =
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? [process.argv[1], `--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
|
? [resolve(process.argv[1]), `--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
|
||||||
: [`--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
|
: [`--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
|
||||||
await sendToDaemon(
|
await sendToDaemon(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -146,10 +146,12 @@ const runChatPage = async () => {
|
|||||||
processContext?: { currentCandidate: any } | null;
|
processContext?: { currentCandidate: any } | null;
|
||||||
}) => Promise<void>
|
}) => Promise<void>
|
||||||
initPuppeteer: () => Promise<{ puppeteer: any }>
|
initPuppeteer: () => Promise<{ puppeteer: any }>
|
||||||
|
dismissGovernanceNoticeDialog: (page: any) => Promise<void>
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
startBossChatPageProcess,
|
startBossChatPageProcess,
|
||||||
initPuppeteer
|
initPuppeteer,
|
||||||
|
dismissGovernanceNoticeDialog
|
||||||
} = (await import('@geekgeekrun/boss-auto-browse-and-chat/index.mjs')) as unknown as BossAutoBrowseModule
|
} = (await import('@geekgeekrun/boss-auto-browse-and-chat/index.mjs')) as unknown as BossAutoBrowseModule
|
||||||
const { setupCanvasTextHook } = (await import('@geekgeekrun/boss-auto-browse-and-chat/resume-extractor.mjs')) as any
|
const { setupCanvasTextHook } = (await import('@geekgeekrun/boss-auto-browse-and-chat/resume-extractor.mjs')) as any
|
||||||
log('boss package import 完成,初始化 puppeteer...')
|
log('boss package import 完成,初始化 puppeteer...')
|
||||||
@@ -252,6 +254,8 @@ const runChatPage = async () => {
|
|||||||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage || {})
|
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage || {})
|
||||||
await page.goto(BOSS_CHAT_PAGE_URL, { timeout: 60 * 1000 })
|
await page.goto(BOSS_CHAT_PAGE_URL, { timeout: 60 * 1000 })
|
||||||
await page.waitForFunction(() => document.readyState === 'complete', { timeout: 120 * 1000 })
|
await page.waitForFunction(() => document.readyState === 'complete', { timeout: 120 * 1000 })
|
||||||
|
await new Promise(r => setTimeout(r, 1500))
|
||||||
|
await dismissGovernanceNoticeDialog(page)
|
||||||
|
|
||||||
sendToDaemon({
|
sendToDaemon({
|
||||||
type: 'worker-to-gui-message',
|
type: 'worker-to-gui-message',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
|
import { resolve } from 'path'
|
||||||
import {
|
import {
|
||||||
ensureStorageFileExist,
|
ensureStorageFileExist,
|
||||||
writeStorageFile,
|
writeStorageFile,
|
||||||
@@ -29,7 +30,7 @@ export async function launchDaemon() {
|
|||||||
daemonProcess = spawn(
|
daemonProcess = spawn(
|
||||||
process.argv[0],
|
process.argv[0],
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? [process.argv[1], `--mode=launchDaemon`]
|
? [resolve(process.argv[1]), `--mode=launchDaemon`]
|
||||||
: [`--mode=launchDaemon`],
|
: [`--mode=launchDaemon`],
|
||||||
{
|
{
|
||||||
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
|
||||||
|
|||||||
Reference in New Issue
Block a user