#!/usr/bin/env node /** * 纯监听模式: 只打开真实验证码页面, **不派发任何鼠标事件**。 * 由人工手动点选, 脚本在后台记录官方 SDK 自己发出的 verify 请求与响应, * 用来判定「人手动过验证码是否也会被 errorCode=12 拒绝」。 * * 用法: node manual_captcha.js [--proxy=http://ip:port] [--minutes=20] */ const { spawn } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); const CHROME = process.env.REA_BROWSER_EXECUTABLE || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; const ORIGIN = process.argv[2] || 'https://1ms.run/'; const AID = process.argv[3] || '195672344'; const PROXY_ARG = process.argv.find(a => a.startsWith('--proxy=')); const PROXY = PROXY_ARG ? PROXY_ARG.slice(8) : null; const MIN_ARG = process.argv.find(a => a.startsWith('--minutes=')); const MINUTES = MIN_ARG ? parseInt(MIN_ARG.slice(10), 10) : 20; const MONITOR_ONLY = process.argv.includes('--monitor-only'); const OUT = '/tmp/1ms'; fs.mkdirSync(OUT, { recursive: true }); const LOG = fs.createWriteStream(path.join(OUT, 'manual.log'), { flags: 'a' }); const say = m => { const line = `[${new Date().toISOString().slice(11, 19)}] ${m}`; process.stderr.write(line + '\n'); LOG.write(line + '\n'); }; const sleep = ms => new Promise(r => setTimeout(r, ms)); async function launchChrome(profile) { const args = ['--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${profile}`, '--window-size=1100,900', '--window-position=80,40']; if (PROXY) args.push(`--proxy-server=${PROXY}`); args.push('about:blank'); const proc = spawn(CHROME, args, { stdio: ['ignore', 'pipe', 'pipe'] }); const portFile = path.join(profile, 'DevToolsActivePort'); for (let i = 0; i < 150; i++) { if (fs.existsSync(portFile)) { const p = parseInt(fs.readFileSync(portFile, 'utf8').split('\n')[0], 10); if (p) return { proc, port: p, profile }; } await sleep(100); } proc.kill('SIGKILL'); throw new Error('Chrome 未就绪'); } class Cdp { constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); this.handlers = []; } static async connect(url) { const ws = new WebSocket(url); await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); const c = new Cdp(ws); ws.onmessage = ev => { const m = JSON.parse(ev.data); if (m.id && c.waits.has(m.id)) { const { res, rej } = c.waits.get(m.id); c.waits.delete(m.id); m.error ? rej(new Error(JSON.stringify(m.error))) : res(m.result); } else if (m.method) c.handlers.forEach(h => h(m)); }; return c; } send(method, params = {}, sessionId) { const id = ++this.id; return new Promise((res, rej) => { this.waits.set(id, { res, rej }); this.ws.send(JSON.stringify({ id, method, params, sessionId })); }); } async eval(expr, sessionId) { const r = await this.send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true }, sessionId); return r.exceptionDetails ? null : r.result.value; } } async function main() { const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'manual-cap-')); const { proc, port } = await launchChrome(profile); try { const ver = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json(); const cdp = await Cdp.connect(ver.webSocketDebuggerUrl); const { targetId } = await cdp.send('Target.createTarget', { url: ORIGIN }); const { sessionId } = await cdp.send('Target.attachToTarget', { targetId, flatten: true }); await cdp.send('Page.enable', {}, sessionId); await cdp.send('Runtime.enable', {}, sessionId); await cdp.send('Network.enable', {}, sessionId); await sleep(3000); const pending = new Set(); let seq = 0; cdp.handlers.push(m => { if (m.method === 'Network.requestWillBeSent' && /cap_union_new_verify/.test(m.params.request.url)) { const n = ++seq; pending.add(m.params.requestId); fs.writeFileSync(path.join(OUT, `manual_verify_req_${n}.txt`), m.params.request.postData || ''); say(`官方 verify #${n} 已发出 (body ${(m.params.request.postData || '').length} 字节)`); } if (m.method === 'Network.loadingFinished' && pending.has(m.params.requestId)) { const id = m.params.requestId; pending.delete(id); (async () => { try { const rb = await cdp.send('Network.getResponseBody', { requestId: id }, sessionId); const txt = rb.base64Encoded ? Buffer.from(rb.body, 'base64').toString('utf8') : rb.body; let code = '?'; try { code = JSON.parse(txt).errorCode; } catch (_) {} const rec = { errorCode: code, response: txt, at: new Date().toISOString() }; fs.writeFileSync(path.join(OUT, 'manual_result.json'), JSON.stringify(rec, null, 2)); say(`★★★★ verify 响应 errorCode=${code} 已写入 ${OUT}/manual_result.json`); } catch (e) { say('取响应体失败: ' + e.message); } })(); } }); if (MONITOR_ONLY) { say('====== [仅监听] 不做任何注入, 请在窗口里登录并走真实签到流程 ======'); say('====== 真实验证码出现后请手动完成, 我会记录 verify 结果 ======'); const until2 = Date.now() + MINUTES * 60000; while (Date.now() < until2) { await sleep(3000); const cb = await cdp.eval('window.__capcb || ""', sessionId).catch(() => ''); if (cb) say('SDK 回调: ' + cb); } return; } await cdp.eval(`(async () => { window.__capcb = null; await new Promise((res, rej) => { const s = document.createElement('script'); s.src = 'https://turing.captcha.qcloud.com/TJCaptcha.js'; s.onload = res; s.onerror = () => rej(new Error('sdk')); document.head.appendChild(s); }); new TencentCaptcha(${JSON.stringify(AID)}, r => { window.__capcb = JSON.stringify(r); document.title = 'CAPCB:' + JSON.stringify(r).slice(0, 80); }).show(); return 'ok'; })()`, sessionId); say('====== 页面已就绪, 验证码已弹出 ======'); say('====== 请手动点选汉字 -> 点「确定」, 我不做任何点击 ======'); say(`====== 代理: ${PROXY || '直连'} 监听 ${MINUTES} 分钟 ======`); const until = Date.now() + MINUTES * 60000; let lastCb = ''; while (Date.now() < until) { await sleep(3000); const cb = await cdp.eval('window.__capcb || ""', sessionId); if (cb && cb !== lastCb) { lastCb = cb; say('★★★ SDK 回调 (成功才会触发): ' + cb); try { fs.writeFileSync(path.join(OUT, 'manual_result.json'), JSON.stringify({ callback: JSON.parse(cb) }, null, 2)); } catch (_) {} } } say('监听结束, 浏览器保持打开'); } catch (e) { say('异常: ' + e.message); } } main().catch(e => { console.error('ERR', e.message); process.exit(1); });