vault backup: 2026-11-06 20:19:57

This commit is contained in:
admin committed 2026-11-06 20:19:58 +08:00
1 parent 8901e4f4db
commit 2c6569dbdf
35 files changed
+17455 -326

No files matched your search

+138
View File
@@ -0,0 +1,138 @@
// 常驻 CDP 桥:只建立一次 WebSocket 连接,之后所有取数都复用这条连接
// 这样 Chrome 的「允许远程调试」只需批准一次。
import fs from 'node:fs';
import path from 'node:path';
import http from 'node:http';
const OUT = path.resolve('.cdp/out');
fs.mkdirSync(OUT, { recursive: true });
const PORT_FILE = path.resolve('.cdp/bridge-port.txt');
const LOG_FILE = path.resolve('.cdp/bridge.log');
const log = (...a) => {
const line = `[${new Date().toISOString()}] ${a.join(' ')}`;
fs.appendFileSync(LOG_FILE, line + '\n', 'utf8');
console.log(line);
};
const UDD = process.env.CHROME_USER_DATA_DIR || 'C:\\Users\\Administrator\\AppData\\Local\\Google\\Chrome\\User Data';
const lines = fs.readFileSync(path.join(UDD, 'DevToolsActivePort'), 'utf8').split('\n').map(s => s.trim()).filter(Boolean);
// ---------- 唯一的 CDP 连接 ----------
let ws = null;
let seq = 0;
const pending = new Map();
function connect() {
return new Promise((resolve, reject) => {
log('正在连接 CDP(此时 Chrome 可能弹出一次「允许」对话框)…');
ws = new WebSocket(`ws://127.0.0.1:${lines[0]}${lines[1]}`);
const timer = setTimeout(() => reject(new Error('CDP 握手超时(60 秒)——请确认已点「允许」')), 60000);
ws.addEventListener('open', () => { clearTimeout(timer); log('CDP 已连接(常驻,后续不再需要批准)'); resolve(); }, { once: true });
ws.addEventListener('error', () => { clearTimeout(timer); reject(new Error('CDP 连接失败')); }, { once: true });
ws.addEventListener('close', (e) => {
log('CDP 连接已关闭 code=' + e.code);
ws = null;
for (const [, p] of pending) { clearTimeout(p.timer); p.rej(new Error('CDP 连接已关闭')); }
pending.clear();
});
ws.addEventListener('message', (ev) => {
let m; try { m = JSON.parse(ev.data); } catch { return; }
if (m.id && pending.has(m.id)) {
const p = pending.get(m.id); pending.delete(m.id); clearTimeout(p.timer);
m.error ? p.rej(new Error(m.error.message)) : p.res(m.result);
}
});
});
}
function send(method, params = {}, sessionId, timeoutMs = 180000) {
if (!ws) return Promise.reject(new Error('CDP 未连接'));
const id = ++seq;
return new Promise((res, rej) => {
const timer = setTimeout(() => { pending.delete(id); rej(new Error('timeout: ' + method)); }, timeoutMs);
pending.set(id, { res, rej, timer });
const msg = { id, method, params };
if (sessionId) msg.sessionId = sessionId;
ws.send(JSON.stringify(msg));
});
}
// ---------- 高层动作:在指定页面执行 JS ----------
async function evalOnPage({ expr, match = 'seller.goofish.com', createUrl, waitMs = 4000, keepTab = false }) {
const { targetInfos } = await send('Target.getTargets');
let target = targetInfos.find(t => t.type === 'page' && new RegExp(match, 'i').test(t.url));
let created = false;
if (!target) {
if (!createUrl) throw new Error(`没有匹配 /${match}/ 的标签页,且未提供 createUrl`);
const r = await send('Target.createTarget', { url: createUrl, background: true });
target = { targetId: r.targetId, url: createUrl };
created = true;
await new Promise(r2 => setTimeout(r2, waitMs));
}
const { sessionId } = await send('Target.attachToTarget', { targetId: target.targetId, flatten: true });
try {
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }, sessionId);
if (r.exceptionDetails) {
return { ok: false, error: r.exceptionDetails.text, detail: r.exceptionDetails.exception?.description || null };
}
return { ok: true, value: r.result?.value, pageUrl: target.url };
} finally {
await send('Target.detachFromTarget', { sessionId }).catch(() => {});
if (created && !keepTab) await send('Target.closeTarget', { targetId: target.targetId }).catch(() => {});
}
}
// ---------- HTTP 接口 ----------
const readBody = req => new Promise((res, rej) => {
let b = '';
req.on('data', c => { b += c; });
req.on('end', () => { try { res(b ? JSON.parse(b) : {}); } catch (e) { rej(e); } });
req.on('error', rej);
});
const server = http.createServer(async (req, res) => {
const json = (code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(obj)); };
try {
if (req.url === '/health') {
const ver = ws ? await send('Browser.getVersion').catch(() => null) : null;
return json(200, { connected: !!ws, browser: ver?.product || null });
}
if (req.url === '/eval' && req.method === 'POST') {
const body = await readBody(req);
const r = await evalOnPage(body);
// 把结果落盘,避免 PowerShell 控制台中文乱码;只回文件路径
if (body.saveAs) {
fs.writeFileSync(path.join(OUT, body.saveAs), JSON.stringify(r, null, 2), 'utf8');
return json(200, { ok: r.ok, savedAs: '.cdp/out/' + body.saveAs, error: r.error || null, pageUrl: r.pageUrl || null });
}
return json(200, r);
}
if (req.url === '/raw' && req.method === 'POST') {
const body = await readBody(req);
const r = await send(body.method, body.params || {});
if (body.saveAs) {
fs.writeFileSync(path.join(OUT, body.saveAs), JSON.stringify(r, null, 2), 'utf8');
return json(200, { savedAs: '.cdp/out/' + body.saveAs });
}
return json(200, r);
}
return json(404, { error: 'not found' });
} catch (e) {
return json(500, { ok: false, error: String(e && e.message || e) });
}
});
try {
fs.unlinkSync(LOG_FILE);
} catch {}
await connect();
server.listen(0, '127.0.0.1', () => {
const port = server.address().port;
fs.writeFileSync(PORT_FILE, String(port), 'utf8');
log('桥接已启动,端口 ' + port + '(已写入 .cdp/bridge-port.txt)');
});
// 保持进程存活
setInterval(() => { }, 1 << 30);