diff --git a/scripts/daily-runner.js b/scripts/daily-runner.js index bb85224..a0c668d 100644 --- a/scripts/daily-runner.js +++ b/scripts/daily-runner.js @@ -164,6 +164,7 @@ function maybeSendReport(myStatus, chatLeft) { function statusText(s) { if (s === 'kicked_out') return '🚨 被顶号(他处登录)'; if (s === 'login_expired') return '🚨 登录失效'; + if (s === 'platform_blocked') return '⛔ 平台阻碍(限额等)'; if (s === 'qr_expired') return '🚨 等扫码但二维码过期'; if (s === 'waiting_relogin') return '⏳ 等待扫码登录'; if (s === 'stopped_by_user') return '⏹️ 手动停止'; @@ -210,6 +211,45 @@ async function waitForRelogin() { return false; } +// 阻碍二次核查(v10.6):重新打开一个候选人弹窗,看是否仍是无效内容页 +// 返回:确认原因字符串(确认阻碍) / null(未复现,可继续) +async function verifyBlockedPage() { + const pw = require('playwright-core'); + try { + const b = await pw.chromium.connectOverCDP(`http://127.0.0.1:${PORT}`); + const p = b.contexts()[0]?.pages().find(x => /rd\d?\.zhaopin\.com/.test(x.url())); + if (!p) return '页面不可达'; + const r = await p.evaluate(() => { + const modal = document.querySelector('.new-shortcut-resume__modal'); + if (!modal) return null; // 弹窗已关(子进程退出时关了)→ 打开一个再验 + const text = modal.innerText || ''; + if (/安全限额|查看.*上限|今日.*上限|明天再/.test(text)) return '日浏览限额'; + return null; + }).catch(() => null); + // 弹窗关了:主动点开第一个卡片再验一次 + if (r === null) { + await p.evaluate(() => document.querySelector('.recommend-item .resume-item__content')?.click()).catch(() => {}); + await new Promise(rr => setTimeout(rr, 3000)); + const r2 = await p.evaluate(() => { + const modal = document.querySelector('.new-shortcut-resume__modal'); + const text = modal?.innerText || ''; + if (!modal) return '弹窗无法打开'; + // 同样的有效性判定:缺结构信号 = 无效页 + const hasNameAge = /\d{2}岁/.test(text) || /(本科|大专|硕士|博士|高中|中专)/.test(text); + if (!hasNameAge) { + if (/安全限额|查看.*上限|明日再|明天再/.test(text)) return '日浏览限额'; + return `无效弹窗: ${text.split('\n').filter(Boolean).slice(0, 2).join('/').slice(0, 60)}`; + } + return null; // 弹窗正常 → 未复现 + }).catch(() => null); + try { await p.evaluate(() => document.querySelector('.new-shortcut-resume__close')?.click()).catch(() => {}); } catch (_) {} + return r2; + } + try { await b.close(); } catch (_) {} + return r; + } catch (e) { return `核查异常: ${String(e).slice(0, 60)}`; } +} + // ---------- 主流程 ---------- (async () => { log(`启动 | 端口${PORT} | 库${DB} | 截止北京18:00`); @@ -250,6 +290,18 @@ async function waitForRelogin() { maybeSendReport('login_expired', lastChatLeft); needRelogin = true; } + // 阻碍即停(v10.6 通用):弹窗无有效简历(限额/提示页等)→ 二次核查确认 → 私聊告警,停止 + // 注意:与风控/验证码分开处理——这种是平台功能限制(如日浏览1000份上限),不是风控 + else if (/弹窗无有效简历数据/.test(out)) { + log('弹窗无有效数据,二次核查中...'); + const confirmed = await verifyBlockedPage(); + if (confirmed) { + send(DM, `🚨 ${ACCT} 碰到平台阻碍(${confirmed}),已核查确认,任务停止。请人工查看任务机浏览器确认后再启动\n日志: ${out.match(/弹窗内容: [^\n]*/)?.[0] || ''}`); + maybeSendReport('platform_blocked', lastChatLeft); + break; + } + log('核查未复现(可能是瞬时异常页),跳过继续下一批'); + } // 风控/验证码 → 私聊告警,结束(不监听,风控需人工评估) else if (/ALERT.*(风控|验证|频繁|受限)/.test(out)) { send(DM, `🚨 ${ACCT} 触发风控/验证码(${(out.match(/ALERT[::][^\n]*/) || ['未知原因'])[0]}),今日筛选中止`); diff --git a/scripts/zhaopin-scrape.js b/scripts/zhaopin-scrape.js index e710087..ccd35a6 100755 --- a/scripts/zhaopin-scrape.js +++ b/scripts/zhaopin-scrape.js @@ -253,7 +253,16 @@ async function extractModalData(page) { const cm = (logsCountEl.innerText || '').match(/\((\d+)条\)/); if (cm) staffLogCount = parseInt(cm[1], 10); } + // ===== 阻碍即停:弹窗有效性核查(v10.6 通用机制)===== + // 正常简历弹窗:有姓名+年龄/学历等结构信号、内容较长、含标准段落 + // 异常弹窗(限额页/提示页/空数据):短文案、无结构信号 + // 触发条件:缺姓名 或 缺全部结构信号(年龄/学历/经验/教育段/工作段)——即"这不是一份简历" + const hasName = !!(nameMatch && nameMatch[1] && nameMatch[1].trim() && !/提示|通知|警告|限额|上限/.test(nameMatch[1])); + const hasStructure = !!(ageMatch || eduMatch || expMatch || eduSection || + section('工作经历', '教育经历|个人优势|专业技能|语言能力|证书|打电话|打招呼')); + const modalBlocked = !hasName || !hasStructure; return { + modalBlocked, staffLogCount, name: nameMatch ? nameMatch[1].trim() : '', age: ageMatch ? parseInt(ageMatch[1], 10) : null, @@ -442,6 +451,12 @@ async function scrapeZhaopin(maxCount = 20, fullMode = false, scoreOnly = false) if (refreshed) { await closeModal(page); log('页面已刷新(列表变化),重扫'); currentDataIndex = null; continue; } const data = await extractModalData(page); + // v10.6 阻碍即停:弹窗不是有效简历(限额/提示页/空数据)→ 不入库,立即熔断 + if (data?.modalBlocked) { + const snippet = (data.rawText || '').split('\n').filter(Boolean).slice(0, 3).join(' / ').slice(0, 120); + await closeModal(page); + return stop(`🚨 ALERT: 弹窗无有效简历数据(可能触发平台限制),弹窗内容: ${snippet || '(空)'}`); + } if (!data?.name) { await closeModal(page); log('数据提取失败'); errors++; currentDataIndex = await navigateToNextCard(page, currentDataIndex); continue; } await humanBrowseModal(page);