diff --git a/百家/.cdp/inspect-page.mjs b/百家/.cdp/inspect-page.mjs new file mode 100644 index 0000000..85ee506 --- /dev/null +++ b/百家/.cdp/inspect-page.mjs @@ -0,0 +1,29 @@ +// 检查百家号页面状态:登录态、页面文字、可用的数据入口 +import { connect, attachPage, evaluate, saveJson } from './lib.mjs'; + +const cdp = await connect(); +const hit = await attachPage(cdp, 'baijiahao.baidu.com'); +if (!hit) { console.log('NO_BAIJIAHAO_TAB'); process.exit(1); } + +const { sessionId } = hit; +await cdp.send('Runtime.enable', {}, sessionId).catch(() => {}); + +const info = await evaluate(cdp, sessionId, `(() => { + const t = document.body ? document.body.innerText : ''; + return { + url: location.href, + title: document.title, + textLen: t.length, + textSample: t.slice(0, 3000), + links: Array.from(document.querySelectorAll('a')).slice(0, 60).map(a => ({ + text: (a.innerText || '').trim().slice(0, 40), + href: a.href + })).filter(x => x.href) + }; +})()`); + +saveJson('out/page-info.json', info); +console.log('URL=' + info.url.replace(/[^\x20-\x7E]/g, '?')); +console.log('TEXTLEN=' + info.textLen); +console.log('SAVED out/page-info.json'); +process.exit(0); diff --git a/百家/.cdp/lib.mjs b/百家/.cdp/lib.mjs new file mode 100644 index 0000000..616907f --- /dev/null +++ b/百家/.cdp/lib.mjs @@ -0,0 +1,100 @@ +// CDP 客户端:读 DevToolsActivePort,建立 WebSocket 连接,提供 evaluate 能力 +import fs from 'node:fs'; +import path from 'node:path'; + +const CHROME_USER_DATA = 'C:/Users/Administrator/AppData/Local/Google/Chrome/User Data'; + +export function readDevToolsPort(userDataDir = CHROME_USER_DATA) { + const p = path.join(userDataDir, 'DevToolsActivePort'); + const raw = fs.readFileSync(p, 'utf8').trim().split(/\r?\n/); + const port = Number(raw[0]); + const browserPath = raw[1]; + if (!port || !browserPath) throw new Error('DevToolsActivePort invalid: ' + raw.join('|')); + return { port, browserPath, wsUrl: `ws://127.0.0.1:${port}${browserPath}` }; +} + +class Cdp { + constructor(ws) { + this.ws = ws; + this.id = 0; + this.pending = new Map(); + this.events = []; + ws.addEventListener('message', (ev) => { + const msg = JSON.parse(ev.data); + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id); + this.pending.delete(msg.id); + msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result); + } else if (msg.method) { + this.events.push(msg); + } + }); + } + + send(method, params = {}, sessionId) { + const id = ++this.id; + const payload = { id, method, params }; + if (sessionId) payload.sessionId = sessionId; + this.ws.send(JSON.stringify(payload)); + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + setTimeout(() => { + if (this.pending.has(id)) { + this.pending.delete(id); + reject(new Error(`timeout: ${method}`)); + } + }, 90000); + }); + } +} + +export async function connect({ timeoutMs = 40000 } = {}) { + const { wsUrl, port } = readDevToolsPort(); + const ws = new WebSocket(wsUrl); + const opened = new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error('ws open timeout')), timeoutMs); + ws.addEventListener('open', () => { clearTimeout(t); resolve(); }); + ws.addEventListener('error', (e) => { clearTimeout(t); reject(new Error('ws error')); }); + }); + await opened; + const cdp = new Cdp(ws); + cdp.port = port; + return cdp; +} + +export async function listPages(cdp) { + const { targetInfos } = await cdp.send('Target.getTargets'); + return targetInfos.filter(t => t.type === 'page'); +} + +export async function attachPage(cdp, urlMatch, { create = null } = {}) { + let pages = await listPages(cdp); + let target = pages.find(t => t.url.includes(urlMatch)); + if (!target && create) { + const { targetId } = await cdp.send('Target.createTarget', { url: create, background: false }); + await new Promise(r => setTimeout(r, 3500)); + pages = await listPages(cdp); + target = pages.find(t => t.targetId === targetId) || pages.find(t => t.url.includes(urlMatch)); + } + if (!target) return null; + const { sessionId } = await cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true }); + return { target, sessionId }; +} + +export async function evaluate(cdp, sessionId, expression, { awaitPromise = true } = {}) { + const res = await cdp.send('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise, + userGesture: true, + }, sessionId); + if (res.exceptionDetails) { + throw new Error('EVAL: ' + (res.exceptionDetails.exception?.description || res.exceptionDetails.text)); + } + return res.result?.value; +} + +export function saveJson(file, data) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8'); +} diff --git a/百家/.cdp/probe.mjs b/百家/.cdp/probe.mjs new file mode 100644 index 0000000..ef9f35b --- /dev/null +++ b/百家/.cdp/probe.mjs @@ -0,0 +1,13 @@ +// 探测:列出所有页面,并找到百家号相关标签 +import { connect, listPages } from './lib.mjs'; + +const cdp = await connect(); +console.log('CDP connected, port=' + cdp.port); + +const pages = await listPages(cdp); +console.log('pages=' + pages.length); +for (const p of pages) { + const line = `${p.targetId.slice(0, 8)} | ${p.title.slice(0, 50)} | ${p.url.slice(0, 120)}`; + console.log('PAGE ' + line.replace(/[^\x20-\x7E|]/g, '?')); +} +process.exit(0);