v10.12 修复hr1打招呼失败: 开弹窗改Playwright原生点击(合成click被平台反自动化检测拦截返回空壳弹窗); 新增空壳弹窗识别+重试+跳过逻辑
This commit is contained in:
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user