diff --git a/scripts/diag-greet.js b/scripts/diag-greet.js new file mode 100644 index 0000000..be3b7fc --- /dev/null +++ b/scripts/diag-greet.js @@ -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); }); diff --git a/scripts/diag-hr1.js b/scripts/diag-hr1.js new file mode 100644 index 0000000..3901de4 --- /dev/null +++ b/scripts/diag-hr1.js @@ -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); }); diff --git a/scripts/diag-modal.js b/scripts/diag-modal.js new file mode 100644 index 0000000..cae40c8 --- /dev/null +++ b/scripts/diag-modal.js @@ -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); }); diff --git a/scripts/diag-modal2.js b/scripts/diag-modal2.js new file mode 100644 index 0000000..c5e7e51 --- /dev/null +++ b/scripts/diag-modal2.js @@ -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); }); diff --git a/scripts/diag-modal3.js b/scripts/diag-modal3.js new file mode 100644 index 0000000..a217bc2 --- /dev/null +++ b/scripts/diag-modal3.js @@ -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); }); diff --git a/scripts/diag-modal4.js b/scripts/diag-modal4.js new file mode 100644 index 0000000..ba0cdf0 --- /dev/null +++ b/scripts/diag-modal4.js @@ -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); }); diff --git a/scripts/open-recommend.js b/scripts/open-recommend.js new file mode 100644 index 0000000..ba5a5fc --- /dev/null +++ b/scripts/open-recommend.js @@ -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); }); diff --git a/scripts/test-greet-hr1.js b/scripts/test-greet-hr1.js new file mode 100644 index 0000000..38e5596 --- /dev/null +++ b/scripts/test-greet-hr1.js @@ -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); }); diff --git a/scripts/zhaopin-scrape.js b/scripts/zhaopin-scrape.js index 942b834..15777b6 100755 --- a/scripts/zhaopin-scrape.js +++ b/scripts/zhaopin-scrape.js @@ -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);