v10.6 阻碍即停(通用): 弹窗有效性核查(缺姓名或全部结构信号=无效页, 不入库)→熔断; runner二次核查确认→私聊告警+停止(platform_blocked状态); 历史662条限额样本回测100%命中/正常简历0误判

This commit is contained in:
hermes
2026-08-27 03:43:54 +00:00
parent 107fa553b6
commit 0b1bd699b5
2 changed files with 67 additions and 0 deletions
+52
View File
@@ -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]}),今日筛选中止`);
+15
View File
@@ -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);