v10.12 修复hr1打招呼失败: 开弹窗改Playwright原生点击(合成click被平台反自动化检测拦截返回空壳弹窗); 新增空壳弹窗识别+重试+跳过逻辑

This commit is contained in:
hermes
2026-09-29 03:12:45 +00:00
parent 1c87c5514d
commit 75438ca50f
9 changed files with 281 additions and 6 deletions
+63
View File
@@ -0,0 +1,63 @@
// 深度诊断:按机器人的完整流程开弹窗→点打招呼,抓每一步的真实状态
const pw = require('playwright-core');
(async () => {
const b = await pw.chromium.connectOverCDP('http://127.0.0.1:9223');
const p = b.contexts()[0].pages().find(x => /recommend/.test(x.url()));
// 1. 找一个没打过招呼的卡片(按钮是"打招呼"而不是"继续沟通")
const idx = await p.evaluate(() => {
const cards = [...document.querySelectorAll('.recommend-item')];
for (let i = 0; i < cards.length; i++) {
const t = cards[i].querySelector('.cv-test-recommend-chat')?.innerText?.trim();
if (t === '打招呼') return i;
}
return -1;
});
console.log('待打招呼卡片索引:', idx);
if (idx < 0) { console.log('当前可见卡片都已打过招呼'); process.exit(0); }
// 2. 开弹窗
await p.evaluate((i) => {
document.querySelectorAll('.recommend-item')[i].querySelector('.resume-item__content')?.click();
}, idx);
await new Promise(r => setTimeout(r, 3500));
// 3. 抓弹窗完整状态
const st = await p.evaluate(() => {
const m = document.querySelector('.new-shortcut-resume__modal');
if (!m) return { modalOpen: false };
const btn = m.querySelector('button.km-button--primary.km-button--filled');
return {
modalOpen: true,
name: m.innerText.split('\n')[0]?.slice(0, 15),
btnText: btn?.innerText?.trim() || '(无)',
btnDisabled: btn?.disabled ?? null,
// 弹窗里有没有任何提示性文案
hints: (m.innerText.match(/(上线|上限|权益|开通|会员|到期|联系顾问)[^\n]{0,20}/g) || []).slice(0, 3),
};
});
console.log('弹窗状态:', JSON.stringify(st, null, 1));
// 4. 点按钮 + 监听点击后的网络请求结果(打招呼API的响应)
if (st.modalOpen && st.btnText === '打招呼') {
const respPromise = p.waitForResponse(r => /greet|chat|hello|im\//i.test(r.url()), { timeout: 12000 }).catch(() => null);
await p.evaluate(() => {
const btn = document.querySelector('.new-shortcut-resume__modal button.km-button--primary.km-button--filled');
btn?.click();
});
const resp = await respPromise;
if (resp) {
let body = '';
try { body = (await resp.text()).slice(0, 200); } catch { }
console.log('API响应:', resp.status(), resp.url().slice(0, 70), body);
} else {
console.log('点击后12秒内无相关网络请求(请求都没发出)');
}
// 最终按钮态
await new Promise(r => setTimeout(r, 3000));
const fin = await p.evaluate(() => document.querySelector('.new-shortcut-resume__modal button.km-button--primary.km-button--filled')?.innerText?.trim());
console.log('最终按钮态:', JSON.stringify(fin));
await p.evaluate(() => document.querySelector('.new-shortcut-resume__close')?.click()).catch(() => { });
}
process.exit(0);
})().catch(e => { console.log('ERR:', String(e).slice(0, 120)); process.exit(1); });
+30
View File
@@ -0,0 +1,30 @@
// 诊断:列出hr1 Chrome的所有标签页和当前推荐页真实状态
const pw = require('playwright-core');
(async () => {
const b = await pw.chromium.connectOverCDP('http://127.0.0.1:9223');
const pages = b.contexts()[0].pages();
for (let i = 0; i < pages.length; i++) {
try {
const url = pages[i].url();
const title = await pages[i].title().catch(() => '');
console.log(`tab${i} | ${url.slice(0, 85)} | ${String(title).slice(0, 25)}`);
} catch (e) { console.log(`tab${i} | (无法读取)`); }
}
// 找推荐页再深度看一次(可能用户手动操作后URL已变)
const p = pages.find(x => /rd\d?\.zhaopin\.com\/app\/recommend/.test(x.url()));
if (p) {
const r = await p.evaluate(() => {
const body = document.body.innerText || '';
const modal = document.querySelector('.new-shortcut-resume__modal');
return {
url: location.href.slice(0, 90),
jobOff: body.includes('未上线'),
chat: document.querySelector('.rights-chat-count .rights-count-number')?.innerText || null,
modalOpen: !!modal,
greetBtn: modal ? (modal.querySelector('button.km-button--primary.km-button--filled')?.innerText || '(无主按钮)') : null,
};
}).catch(e => ({ err: String(e).slice(0, 60) }));
console.log('推荐页:', JSON.stringify(r, null, 1));
}
process.exit(0);
})().catch(e => { console.log('ERR:', String(e).slice(0, 100)); process.exit(1); });
+26
View File
@@ -0,0 +1,26 @@
// 弹窗没打开——换点击方式:Playwright原生click + 多等待
const pw = require('playwright-core');
(async () => {
const b = await pw.chromium.connectOverCDP('http://127.0.0.1:9223');
const p = b.contexts()[0].pages().find(x => /recommend/.test(x.url()));
// 原生点击卡片0的内容区
const card = p.locator('.recommend-item').first();
await card.scrollIntoViewIfNeeded().catch(() => { });
await card.locator('.resume-item__content, .talent-basic-info, div').first().click({ timeout: 8000 }).catch(async e => {
console.log('内容区点击失败,试卡片本身:', String(e).slice(0, 60));
await card.click({ timeout: 8000 }).catch(e2 => console.log('卡片点击也失败:', String(e2).slice(0, 60)));
});
await new Promise(r => setTimeout(r, 4000));
const st = await p.evaluate(() => {
const m = document.querySelector('.new-shortcut-resume__modal');
const anyModal = document.querySelector('.km-modal, [class*=resume-detail], [class*=modal][class*=open]');
return {
targetModal: !!m,
anyModal: !!anyModal,
anyModalCls: anyModal ? String(anyModal.className).slice(0, 60) : null,
url: location.href.slice(0, 100),
};
});
console.log('点击后:', JSON.stringify(st, null, 1));
process.exit(0);
})().catch(e => { console.log('ERR:', String(e).slice(0, 100)); process.exit(1); });
+27
View File
@@ -0,0 +1,27 @@
// 抓那个拦截弹窗的完整文案
const pw = require('playwright-core');
(async () => {
const b = await pw.chromium.connectOverCDP('http://127.0.0.1:9223');
const p = b.contexts()[0].pages().find(x => /recommend/.test(x.url()));
// 先关掉可能开着的,重新触发一次
await p.evaluate(() => {
document.querySelectorAll('.km-modal__close, .km-modal button').forEach(b => { try { b.click(); } catch { } });
}).catch(() => { });
await new Promise(r => setTimeout(r, 1500));
// 点击卡片
await p.evaluate(() => {
document.querySelector('.recommend-item .resume-item__content, .recommend-item')?.click();
}).catch(() => { });
await new Promise(r => setTimeout(r, 3500));
const r = await p.evaluate(() => {
const m = document.querySelector('.km-modal:not([class*=hidden]), .km-modal');
const all = [...document.querySelectorAll('.km-modal')];
return {
count: all.length,
texts: all.map(x => (x.innerText || '(空)').replace(/\n+/g, ' | ').slice(0, 150)),
visible: all.map(x => x.offsetParent !== null),
};
}).catch(e => ({ err: String(e).slice(0, 60) }));
console.log(JSON.stringify(r, null, 1));
process.exit(0);
})().catch(e => { console.log('ERR:', String(e).slice(0, 100)); process.exit(1); });
+24
View File
@@ -0,0 +1,24 @@
// 弹窗DOM存在但innerText为空且不可见——可能是懒渲染。等待内容加载再抓,同时截图直看
const pw = require('playwright-core');
(async () => {
const b = await pw.chromium.connectOverCDP('http://127.0.0.1:9223');
const p = b.contexts()[0].pages().find(x => /recommend/.test(x.url()));
// 重新点击卡片并等足8秒
await p.evaluate(() => document.querySelector('.recommend-item .resume-item__content, .recommend-item')?.click()).catch(() => { });
await new Promise(r => setTimeout(r, 8000));
const st = await p.evaluate(() => {
const m = document.querySelector('.new-shortcut-resume__modal');
const km = document.querySelector('.km-modal--open, .km-modal:not([style*="display: none"])');
return {
resumeModal: !!m,
resumeModalText: m ? (m.innerText || '').replace(/\n/g, '|').slice(0, 200) : null,
kmOpen: !!km,
kmText: km ? (km.innerText || '(空文本)').replace(/\n/g, '|').slice(0, 200) : null,
};
}).catch(e => ({ err: String(e).slice(0, 60) }));
console.log(JSON.stringify(st, null, 1));
// 截图(整页)直看页面到底是什么状态
await p.screenshot({ path: '/opt/data/logs/recruitment/hr1-page-now.png' }).catch(e => console.log('截图失败', String(e).slice(0, 50)));
console.log('截图已存: hr1-page-now.png');
process.exit(0);
})().catch(e => { console.log('ERR:', String(e).slice(0, 100)); process.exit(1); });
+25
View File
@@ -0,0 +1,25 @@
// 终极诊断:拦截弹窗是canvas/图片渲染或iframe——查iframe和shadow DOM
const pw = require('playwright-core');
(async () => {
const b = await pw.chromium.connectOverCDP('http://127.0.0.1:9223');
const p = b.contexts()[0].pages().find(x => /recommend/.test(x.url()));
const st = await p.evaluate(() => {
const m = document.querySelector('.km-modal--open, .km-modal:not([style*="display: none"])');
if (!m) return { noModal: true };
// 弹窗内部结构全景
const walk = (el, depth) => {
if (!el || depth > 3) return '';
let out = `${'.'.repeat(depth)}<${el.tagName.toLowerCase()} class="${String(el.className).slice(0, 40)}" text="${(el.childNodes.length===1&&el.textContent||'').trim().slice(0,30)}">\n`;
if (el.tagName === 'IFRAME') out += `${'.'.repeat(depth+1)}IFRAME src=${el.src?.slice(0, 80)}\n`;
for (const c of el.children || []) out += walk(c, depth + 1);
return out;
};
const iframes = [...document.querySelectorAll('iframe')].map(f => ({ src: f.src?.slice(0, 70), visible: f.offsetParent !== null }));
return {
modalHTML: m.outerHTML.slice(0, 600),
iframes,
};
}).catch(e => ({ err: String(e).slice(0, 80) }));
console.log(JSON.stringify(st, null, 1).slice(0, 1200));
process.exit(0);
})().catch(e => { console.log('ERR:', String(e).slice(0, 100)); process.exit(1); });
+19
View File
@@ -0,0 +1,19 @@
// 打开 hr1 推荐页(若已存在则复用)
const { chromium } = require('playwright-core');
const URL = process.argv[2] || 'https://rd6.zhaopin.com/app/recommend?jobNumber=CC634000780J40731895116&tab=recommend';
(async () => {
const res = await fetch('http://127.0.0.1:9223/json/version');
const info = await res.json();
const browser = await chromium.connectOverCDP(info.webSocketDebuggerUrl);
const ctx = browser.contexts()[0];
let page = ctx.pages().find(p => p.url().includes('recommend'));
if (!page) {
page = await ctx.newPage();
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(e => console.log('goto:', e.message));
}
await page.waitForTimeout(8000);
console.log('final url:', page.url());
const body = (await page.evaluate(() => document.body?.innerText?.slice(0, 300)).catch(() => ''));
console.log('body head:', JSON.stringify((body || '').split('\n').filter(Boolean).slice(0, 8)));
await browser.close();
})().catch(e => { console.error(e.message); process.exit(1); });
+37
View File
@@ -0,0 +1,37 @@
// 手动测试 hr1 打招呼是否恢复(职位切换后验证)
const pw = require('playwright-core');
(async () => {
const b = await pw.chromium.connectOverCDP('http://127.0.0.1:9223');
const p = b.contexts()[0]?.pages().find(x => /rd\d?\.zhaopin\.com/.test(x.url()));
const cards = await p.$$('.recommend-item');
console.log('卡片数:', cards.length);
for (let i = 0; i < Math.min(cards.length, 4); i++) {
const content = await cards[i].$('.resume-item__content').catch(() => null);
if (!content) continue;
await content.click();
await new Promise(r => setTimeout(r, 3500));
const btn = p.locator('.new-shortcut-resume__modal button.km-button--primary.km-button--filled').first();
const t1 = ((await btn.innerText({ timeout: 4000 }).catch(() => '')) || '').trim();
console.log(`卡片${i} 按钮文案:`, JSON.stringify(t1));
if (t1 === '打招呼') {
await btn.click({ timeout: 5000 }).catch(e => console.log('点击失败:', String(e).slice(0, 60)));
let final = '';
for (let k = 0; k < 16; k++) {
await new Promise(r => setTimeout(r, 500));
final = ((await btn.innerText({ timeout: 2000 }).catch(() => '')) || '').trim();
if (final === '继续沟通') break;
}
console.log(final === '继续沟通' ? '✅ 打招呼成功!职位已生效' : '❌ 点击后按钮仍为: ' + (final || '(空)'));
await p.evaluate(() => document.querySelector('.new-shortcut-resume__close')?.click()).catch(() => { });
process.exit(0);
} else if (t1 === '继续沟通') {
console.log('该候选人已打过招呼,试下一个');
} else {
console.log('❌ 无打招呼按钮(职位仍未生效)');
process.exit(0);
}
await p.evaluate(() => document.querySelector('.new-shortcut-resume__close')?.click()).catch(() => { });
await new Promise(r => setTimeout(r, 800));
}
process.exit(0);
})().catch(e => { console.log('ERR:', String(e).slice(0, 100)); process.exit(1); });
+30 -6
View File
@@ -447,15 +447,39 @@ async function scrapeZhaopin(maxCount = 20, fullMode = false, scoreOnly = false)
if (cardBlackHit) { log(`🚫 [跳过-列表黑名单] ${card.name} | 卡片含"${cardBlackHit}"`); skipped++; currentDataIndex = await navigateToNextCard(page, currentDataIndex); continue; }
try {
// 打开弹窗
await page.evaluate((di) => {
const el = document.querySelector(`.recommend-item[data-index="${di}"] .resume-item__content`);
(el || document.querySelector(`.recommend-item[data-index="${di}"]`))?.click();
}, currentDataIndex);
// 打开弹窗(v10.12:Playwright原生点击替代evaluate合成click——平台反自动化检测会
// 对合成点击返回空白弹窗;原生点击走真实鼠标事件流 CDP Input.dispatchMouseEvent)
try {
const cardLoc = page.locator(`.recommend-item[data-index="${currentDataIndex}"] .resume-item__content`).first();
await cardLoc.click({ timeout: 8000 });
} catch (_) {
// 兜底:内容区选择器失败时点卡片本身(同样用原生点击)
await page.locator(`.recommend-item[data-index="${currentDataIndex}"]`).first().click({ timeout: 8000 });
}
await randomSleep(3, 5);
if (!await page.evaluate(() => !!document.querySelector('.km-modal--open') || !!document.querySelector('.km-modal__backdrop--active'))) {
// 弹窗有效性双检:不仅看"有弹窗",还要看是简历弹窗(空壳km-modal说明被拦截)
const modalOk = await page.evaluate(() => ({
open: !!document.querySelector('.km-modal--open, .km-modal__backdrop--active'),
isResume: !!document.querySelector('.new-shortcut-resume__modal'),
}));
if (!modalOk.open) {
log('弹窗未打开,跳过'); errors++; currentDataIndex = await navigateToNextCard(page, currentDataIndex); continue;
}
if (!modalOk.isResume) {
// 空壳弹窗:关掉重试一次原生点击;再不行跳过该人
log('弹窗为空壳(非简历弹窗),关闭重试');
await closeModal(page);
await randomSleep(2, 3);
try {
await page.locator(`.recommend-item[data-index="${currentDataIndex}"] .resume-item__content`).first().click({ timeout: 8000 });
await randomSleep(3, 5);
} catch (_) { }
const retryOk = await page.evaluate(() => !!document.querySelector('.new-shortcut-resume__modal'));
if (!retryOk) {
await closeModal(page);
log('重试仍是空壳弹窗,跳过该卡片'); errors++; currentDataIndex = await navigateToNextCard(page, currentDataIndex); continue;
}
}
// 平台唯一标识
const resumeNumber = extractResumeNumber(page);