mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-05 15:38:46 +08:00
feat(boss): 招聘端反检测增强 — 验证码 in-loop 等待 + 指纹/行为加固
## 变更汇总 **核心修复**:验证码出现时不再 crash → 关浏览器 → 3 秒重启(连环触发),改为在主循环内等待用户手动完成(最长 5 分钟),完成后无缝继续。 **行为层**(默认开启): - ghost-cursor fail-fast(防静默降级为裸 page.click()) - 点击落点在按钮中心 60% 区域内随机化 + ~50% 概率 micro-overshoot - 每次会话启动时初始鼠标位置随机化(三处 launch 全部覆盖) - 每位候选人处理后做一次 checkpointRiskControl;软风控(风控/操作频繁)和硬验证码均可检测,均走 in-loop 等待 **指纹层**(默认开启): - Launch args:--lang=zh-CN、--disable-blink-features=AutomationControlled - Canvas hook marker 名(window.__cct_<random>)每次会话随机,[canvasHook] 日志默认静默 - fillText wrapper 通过 laodeng.registerFakeNativeSource 注册到 toString 伪装表 **指纹层**(可选,默认关): - boss-recruiter.json advanced.persistProfile:持久化 Chromium profile,BOSS 看到「老设备」 - UI 加「高级反检测(实验性)」开关,IPC 支持保存 **关联修复**(Copilot + internal review 发现): - RISK_CONTROL 软风控文案覆盖到 detectRiskControl - window.__canvasCapturedText 直读路径全面迁移:新增 peekCapturedText(不消费的计数查询)、getResumeData 接收 getCapturedText closure - persistProfile=true 时两处 launch 均跳过 setCookie 新增文件:risk-detector.mjs、launch-options.mjs 不影响应聘端(geek-auto-start-chat-with-boss)。
This commit is contained in:
@@ -140,7 +140,7 @@ const runChatPage = async () => {
|
||||
log('正在动态 import boss package...')
|
||||
type BossAutoBrowseModule = {
|
||||
startBossChatPageProcess: (hooks: any, options?: {
|
||||
browser?: any; page?: any; getCapturedText?: any; clearCapturedText?: any;
|
||||
browser?: any; page?: any; getCapturedText?: any; clearCapturedText?: any; peekCapturedText?: any;
|
||||
jobId?: string | null;
|
||||
retryCandidate?: { encryptGeekId: string; geekName: string; jobTitle: string } | null;
|
||||
processContext?: { currentCandidate: any } | null;
|
||||
@@ -207,6 +207,7 @@ const runChatPage = async () => {
|
||||
let page: any = null
|
||||
let getCapturedText: any = null
|
||||
let clearCapturedText: any = null
|
||||
let peekCapturedText: any = null
|
||||
// processContext 提升到循环外,catch 块中可读取被中断的候选人
|
||||
const processContext: { currentCandidate: any } = { currentCandidate: null }
|
||||
|
||||
@@ -230,13 +231,12 @@ const runChatPage = async () => {
|
||||
log('启动浏览器...')
|
||||
await hooks.beforeBrowserLaunch?.promise?.()
|
||||
|
||||
const headless = process.env.HEADLESS === '1'
|
||||
browser = await puppeteer.launch({
|
||||
headless,
|
||||
ignoreHTTPSErrors: true,
|
||||
protocolTimeout: 120000,
|
||||
defaultViewport: { width: 1440, height: 900 - 140 }
|
||||
})
|
||||
const { buildRecruiterLaunchOptions } = (await import(
|
||||
'@geekgeekrun/boss-auto-browse-and-chat/launch-options.mjs'
|
||||
)) as any
|
||||
const launchOpts = await buildRecruiterLaunchOptions()
|
||||
log(`使用 launch options:persistProfile=${!!launchOpts.userDataDir}`)
|
||||
browser = await puppeteer.launch(launchOpts)
|
||||
|
||||
await hooks.afterBrowserLaunch?.promise?.()
|
||||
|
||||
@@ -248,7 +248,15 @@ const runChatPage = async () => {
|
||||
const canvasHooks = await setupCanvasTextHook(page)
|
||||
getCapturedText = canvasHooks.getCapturedText
|
||||
clearCapturedText = canvasHooks.clearCapturedText
|
||||
if (Array.isArray(bossCookies) && bossCookies.length > 0) {
|
||||
peekCapturedText = canvasHooks.peekCapturedText
|
||||
|
||||
const { randomizeInitialCursorPosition } = (await import(
|
||||
'@geekgeekrun/boss-auto-browse-and-chat/humanMouse.mjs'
|
||||
)) as any
|
||||
await randomizeInitialCursorPosition(page).catch(() => {})
|
||||
|
||||
// persistProfile=true 时 profile 已持久化 cookies,跳过注入避免用过期文件覆盖有效 session
|
||||
if (!launchOpts.userDataDir && Array.isArray(bossCookies) && bossCookies.length > 0) {
|
||||
await page.setCookie(...bossCookies)
|
||||
}
|
||||
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage || {})
|
||||
@@ -287,7 +295,7 @@ const runChatPage = async () => {
|
||||
const jname = job.jobName ?? job.name
|
||||
log(`开始处理职位 ${jid}(${jname})的沟通页...`)
|
||||
processContext.currentCandidate = null
|
||||
await startBossChatPageProcess(hooks, { browser, page, getCapturedText, clearCapturedText, jobId: jid, processContext })
|
||||
await startBossChatPageProcess(hooks, { browser, page, getCapturedText, clearCapturedText, peekCapturedText, jobId: jid, processContext })
|
||||
log(`职位 ${jid} 沟通页处理完成`)
|
||||
}
|
||||
} else {
|
||||
@@ -296,7 +304,7 @@ const runChatPage = async () => {
|
||||
} else {
|
||||
log('未配置职位队列,开始执行 startBossChatPageProcess(处理所有未读)...')
|
||||
processContext.currentCandidate = null
|
||||
await startBossChatPageProcess(hooks, { browser, page, getCapturedText, clearCapturedText, processContext })
|
||||
await startBossChatPageProcess(hooks, { browser, page, getCapturedText, clearCapturedText, peekCapturedText, processContext })
|
||||
}
|
||||
log('startBossChatPageProcess 完成')
|
||||
|
||||
@@ -318,6 +326,7 @@ const runChatPage = async () => {
|
||||
page = null
|
||||
getCapturedText = null
|
||||
clearCapturedText = null
|
||||
peekCapturedText = null
|
||||
const rerunMs = cfg?.chatPage?.rerunIntervalMs ?? rerunInterval
|
||||
log(`下次运行将在 ${rerunMs}ms 后开始`)
|
||||
await sleep(rerunMs)
|
||||
@@ -346,7 +355,7 @@ const runChatPage = async () => {
|
||||
log(`🔄 正在重试被验证中断的候选人:${interruptedCandidate.geekName}...`)
|
||||
try {
|
||||
await startBossChatPageProcess(hooks, {
|
||||
browser, page, getCapturedText, clearCapturedText,
|
||||
browser, page, getCapturedText, clearCapturedText, peekCapturedText,
|
||||
retryCandidate: interruptedCandidate,
|
||||
processContext: { currentCandidate: null }
|
||||
})
|
||||
@@ -369,6 +378,7 @@ const runChatPage = async () => {
|
||||
page = null
|
||||
getCapturedText = null
|
||||
clearCapturedText = null
|
||||
peekCapturedText = null
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('LOGIN_STATUS_INVALID')) {
|
||||
|
||||
@@ -991,6 +991,12 @@ export default function initIpc() {
|
||||
...payload.recommendPage
|
||||
}
|
||||
}
|
||||
if (payload.advanced && typeof payload.advanced === 'object') {
|
||||
bossRecruiterConfig.advanced = bossRecruiterConfig.advanced || {}
|
||||
if (typeof payload.advanced.persistProfile === 'boolean') {
|
||||
bossRecruiterConfig.advanced.persistProfile = payload.advanced.persistProfile
|
||||
}
|
||||
}
|
||||
|
||||
const candidateFilterConfig = readBossConfigFile('candidate-filter.json') || {}
|
||||
if (hasOwn(payload, 'expectCityList')) {
|
||||
|
||||
@@ -90,6 +90,22 @@
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="config-section">
|
||||
<el-form-item mb0>
|
||||
<div class="section-title">高级反检测(实验性)</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="formContent.advancedPersistProfile">
|
||||
持久化浏览器 profile(更难被识别为新设备)
|
||||
</el-checkbox>
|
||||
<div class="form-tip">
|
||||
启用后 BOSS 看到的是「老设备」而非「每次都是新设备」,能显著降低人工验证触发率。<br>
|
||||
副作用:bot 运行期间不能在系统 Chrome 同时登录 BOSS(会被挤掉);profile 文件夹长期会占用 1-2GB 磁盘空间。<br>
|
||||
路径:<code>~/.geekgeekrun/storage/boss-chrome-profile/</code>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<div class="action-bar">
|
||||
<el-button :loading="isSaving" @click="handleSave">仅保存配置</el-button>
|
||||
<el-button type="primary" :loading="isSaving" @click="handleSubmit">
|
||||
@@ -167,7 +183,8 @@ const formContent = reactive({
|
||||
recommendSkipViewedCandidates: false,
|
||||
recommendRerunIntervalMs: 3000,
|
||||
recommendDelayBetweenNotInterestedMs: [800, 2500] as [number, number],
|
||||
recommendKeepBrowserOpenAfterRun: false
|
||||
recommendKeepBrowserOpenAfterRun: false,
|
||||
advancedPersistProfile: false
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -194,6 +211,9 @@ onMounted(async () => {
|
||||
: [800, 2500]
|
||||
formContent.recommendKeepBrowserOpenAfterRun =
|
||||
recommendPage.keepBrowserOpenAfterRun ?? false
|
||||
|
||||
const advanced = recruiterConfig.advanced ?? {}
|
||||
formContent.advancedPersistProfile = advanced.persistProfile ?? false
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@@ -213,6 +233,9 @@ const doSave = async () => {
|
||||
rerunIntervalMs: formContent.recommendRerunIntervalMs,
|
||||
delayBetweenNotInterestedMs: formContent.recommendDelayBetweenNotInterestedMs,
|
||||
keepBrowserOpenAfterRun: formContent.recommendKeepBrowserOpenAfterRun
|
||||
},
|
||||
advanced: {
|
||||
persistProfile: formContent.advancedPersistProfile
|
||||
}
|
||||
}
|
||||
await ipcRenderer.invoke('save-boss-recruiter-config', JSON.stringify(payload))
|
||||
|
||||
Reference in New Issue
Block a user