提交 8158cdc3 编写于 作者: R root

Auto Commit

上级 669eac6d
//fetchWithPuppeteer.js
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const { execSync } = require('child_process');
puppeteer.use(StealthPlugin());
const fs = require('fs');
const path = require('path');
const readline = require('readline');
// ============= 共享状态 =============
let pendingVerify = false;
let pageGlobal = null;
let qrCodeContent = '';
let userInfo = "{}";
// 新增:更清晰的扫码状态
let scanStatus = ''; // new | scanned | verify
let scanUser = {}; // 当前扫码用户信息(头像、昵称等)
let lastVerifyError = ''; // 新增:最后一次验证码错误描述
let browserInstance = null;
async function stopBot() {
if (browserInstance) {
try {
console.log(`🛑 正在关闭 Puppeteer...`);
await browserInstance.close();
console.log(`✅ Puppeteer 已关闭`);
} catch (err) {
console.log(`❌ 关闭 Puppeteer 失败:`, err);
}
browserInstance = null; // 一定要重置
} else {
console.log(`⚠️ 没有运行中的 Puppeteer`);
}
browserInstance = null;
pendingVerify = false;
pageGlobal = null;
qrCodeContent = '';
scanStatus = '';
scanUser = {};
lastVerifyError = '';
userInfo = "{}";
console.log(`♻️ 状态已清理,允许重新启动`);
}
async function submitVerifyCode(code) {
console.log(`✅ 收到验证码:${code}`);
await pageGlobal.keyboard.type(code, { delay: 500 });
const [confirmButton] = await pageGlobal.$x(`//div[text()="验证"]`);
if (confirmButton) await confirmButton.click();
pendingVerify = false;
await pageGlobal.screenshot({ path: `img/after_input${code}_${getTimestamp()}.png` });
console.log('✅ 收到验证码,已输入验证码,已截图');
}
/**
* 加载账号专用 Cookie
* @param {import('puppeteer').Page} page
* @param {string} accountName
*/
async function loadCookies(page, accountName) {
const cookiePath = path.join('cookies', `cookies_${accountName}.json`);
if (fs.existsSync(cookiePath)) {
const cookies = JSON.parse(fs.readFileSync(cookiePath));
await page.setCookie(...cookies);
console.log(`🍪 已加载账号【${accountName}】的 Cookie`);
} else {
console.log(`⚠️ 未找到账号【${accountName}】的 Cookie 文件,准备扫码登录`);
}
}
/**
* 保存账号 Cookie
* @param {import('puppeteer').Page} page
* @param {string} accountName
*/
async function saveCookies(page, accountName) {
const cookies = await page.cookies();
const dir = 'cookies';
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
const cookiePath = path.join(dir, `cookies_${accountName}.json`);
fs.writeFileSync(cookiePath, JSON.stringify(cookies, null, 2));
console.log(`💾 已保存账号【${accountName}】的 Cookie: ${cookiePath}`);
}
// 用于格式化时间戳
function getTimestamp() {
const now = new Date();
const YYYY = now.getFullYear();
const MM = String(now.getMonth() + 1).padStart(2, '0');
const DD = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
return `${YYYY}${MM}${DD}_${hh}${mm}${ss}`;
}
function detectChromePath() {
const candidates = ['google-chrome', 'chromium-browser', 'chromium'];
for (const cmd of candidates) {
try {
const path = execSync(`which ${cmd}`).toString().trim();
if (path) {
console.log(`✅ 检测到 Chrome 路径: ${path}`);
return path;
}
} catch (e) {
continue;
}
}
throw new Error("❌ 未检测到可用的 Chrome/Chromium,请先安装!");
}
/**
* 等待出现 + 自动轮询点击指定验证选项
* @param {import('puppeteer').Page} page Puppeteer 页面实例
* @param {string} optionText 按钮文字,如 '接收短信验证码'
* @param {number} timeout 最大等待时间(毫秒),默认 30 秒
* @param {number} interval 检测间隔(毫秒),默认 1000ms
*/
async function clickVerifyOptionAutoRetry(page, optionText, timeout = 30000, interval = 1000) {
const xpath = `//div[contains(text(), "${optionText}")]`;
const start = Date.now();
console.log(`🟢 开始等待【${optionText}】按钮出现...`);
while (Date.now() - start < timeout) {
const [button] = await page.$x(xpath);
if (button) {
await button.click();
console.log(`✅ 已点击【${optionText}】按钮`);
const filename = `img/click_${optionText}_${getTimestamp()}.png`;
await page.screenshot({ path: filename });
console.log(`📸 点击后已截图保存为 ${filename}`);
//输入验证码并点击验证
await checkTimerAndVerifyAutoRetry(page, '888888', 60000, 1000);
return; // 找到后立即结束
} else {
console.log(`⏳ 【${optionText}】按钮暂未出现,继续等待...`);
}
await page.waitForTimeout(interval);
}
console.error(`❌ 超时未找到【${optionText}】按钮`);
}
/**
* 自动轮询:倒计时秒数 <= 30 时,输入验证码并点击验证,每一步都截图保存
* @param {import('puppeteer').Page} page Puppeteer Page 实例
* @param {string} verifyCode 要输入的验证码
* @param {number} timeout 最大等待时长(毫秒)
* @param {number} interval 轮询间隔(毫秒)
*/
async function checkTimerAndVerifyAutoRetry(page, verifyCode = '888888', timeout = 60000, interval = 3000) {
const xpathForTimer = `//span[contains(text(), "后重新发送")]`;
const start = Date.now();
console.log(`🟢 [开始轮询] 检测倒计时...`);
let isKeyinput = false;
let isClickbtt = false;
pageGlobal = page; // 保存给后端接口用
while (Date.now() - start < timeout) {
const [timerElement] = await page.$x(xpathForTimer);
if (timerElement) {
const timerText = await page.evaluate(el => el.textContent, timerElement);
console.log(`⏳ [倒计时文本] ${timerText}`);
// 每次检测都截图
const checkFilename = `img/check_timer_${getTimestamp()}.png`;
await page.screenshot({ path: checkFilename });
console.log(`📸 [已截图] 当前检测保存为 ${checkFilename}`);
const match = timerText.match(/(\d+)s/);
if (match) {
const remaining = parseInt(match[1], 10);
console.log(`⏳ [剩余秒数] ${remaining}s`);
if (remaining <= 50 && !isKeyinput) {
isKeyinput = true;
// ✅ 改这里:不要再 readline,而是挂状态
pendingVerify = true;
console.log('⚡ 已挂 pendingVerify = true,前端页面可以轮询到');
// console.log('✅ [满足条件] 剩余秒数 <= 50,开始输入验证码');
// 模拟真实输入
// await page.keyboard.type('888888', { delay: 100 });
// console.log('✅ 已通过 Puppeteer 原生键盘输入验证码:888888');
// const verifyCode = await waitForUserInput('👉 请在控制台输入收到的验证码:');
// console.log(`✅ 你输入的验证码是:${verifyCode}`);
// // 可以定位到 input 元素(示例)
// await page.keyboard.type(verifyCode, { delay: 100 });
// 可选:截图验证
await page.screenshot({ path: `img/after_原生键盘输入_${getTimestamp()}.png` });
await page.screenshot({ path: `img/after_input_${getTimestamp()}.png` });
console.log('✅ 已输入验证码,已截图');
}else if (remaining <= 45 && !isClickbtt) {
// isClickbtt = true;
// 点击验证按钮
// const xpathForConfirm = `//div[text()="验证"]`;
// const [confirmButton] = await page.$x(xpathForConfirm);
// if (confirmButton) {
// const beforeClickFilename = `img/before_click_confirm_${getTimestamp()}.png`;
// await page.screenshot({ path: beforeClickFilename });
// console.log(`📸 [已截图] 点击前保存为 ${beforeClickFilename}`);
// await confirmButton.click();
// console.log('✅ [已点击] 【验证】按钮');
// const afterClickFilename = `img/after_click_confirm_${getTimestamp()}.png`;
// await page.screenshot({ path: afterClickFilename });
// console.log(`📸 [已截图] 点击后保存为 ${afterClickFilename}`);
// }else {
// console.log('❌ [未找到] 【验证】按钮');
// }
}else {
console.log(`⏳ [继续等待] 剩余秒数 ${remaining} > 30`);
}
} else {
console.log('⚠️ [未匹配] 秒数格式,继续等待...');
}
} else {
console.log('⏳ [未找到] 倒计时元素,继续等待...');
}
const beforeClickFilename = `img/before_[wait]_${getTimestamp()}.png`;
await page.screenshot({ path: beforeClickFilename });
await page.waitForTimeout(interval);//等待
if(isClickbtt && isKeyinput){
const beforeClickFilename = `img/before_[wait]_${getTimestamp()}.png`;
await page.screenshot({ path: beforeClickFilename });
console.log(`[完成了] 倒计时元素[ isClickbtt:${getTimestamp()} isKeyinput:${getTimestamp()} ]`);
// return;
}
}//where
const beforeClickFilename = `img/before_[超时]_${getTimestamp()}.png`;
await page.screenshot({ path: beforeClickFilename });
console.log(`❌ [超时] (${timeout / 1000}s) 未检测到倒计时 <= 30,未执行验证`);
}
async function fetchQrFromPage(entryUrl, accountName = 'userTest') {
if (browserInstance) {
console.log(`♻️ 已有 Puppeteer,先关闭`);
await stopBot(); // 👈 这里是自己安全关闭
}
const chromePath = detectChromePath();
const browser = await puppeteer.launch({
headless: 'new',
executablePath: chromePath,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled',
],
defaultViewport: {
width: 1280,
height: 800
}
});
browserInstance = browser;
const page = await browser.newPage();
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
await page.setExtraHTTPHeaders({
'Accept-Language': 'zh-CN,zh;q=0.9'
});
// 先尝试加载
await loadCookies(page, accountName);
let hookResult = null;
// 监听响应
page.on('response', async (response) => {
const url = response.url();
// console.log('📡 url:', url);
if (url.includes("get_qrcode")) {
console.log('📡 url:', url);
try {
// const data = await response.json();
const text = await response.text();
console.log('📡 原始响应:', text);
hookResult = text;
console.log("🎯 截获二维码接口返回:");
const json = JSON.parse(text);
const base64 = json?.data?.qrcode;
if (base64) {
qrCodeContent = base64; // 保存给前端页面
console.log('✅ 已保存二维码 base64:', base64.substring(0, 30) + '...');
}
} catch (err) {
console.error('❌ 解析响应失败:', err.message);
}
const filename = `img/scree_${getTimestamp()}.png`;
await page.screenshot({ path: filename });
console.log(`📸 错误截图已保存为 ${filename}`);
}
// 监听二维码状态查询接口
if (url.includes('check_qrconnect') || url.includes("check_qrcode")) {
const text = await response.text();
console.log('📡 原始响应:', text);
const json = JSON.parse(text);
if (json?.data?.status === 'new') {
scanStatus = 'new';
pendingVerify = false;
}
if (json?.data?.status === 'scanned') {
scanStatus = 'scanned';
scanUser = json.data.scan_user_info || {};
pendingVerify = false;
}
if (json?.data?.account_flow === 'verify') {
scanStatus = 'verify';
pendingVerify = true;
await clickVerifyOptionAutoRetry(page, '接收短信验证码');
}
// 已过期可选:刷新二维码
if (json?.data?.status === 'expired' && json?.data?.qrcode) {
qrCodeContent = json.data.qrcode;
console.log('♻️ 二维码已过期,已更新 base64');
await page.reload({ waitUntil: 'networkidle2' });
}
}
if (url.includes('validate_code')) {
const text = await response.text();
console.log('📡 验证码校验响应:', text);
try {
const json = JSON.parse(text);
if (json?.message === 'error') {
lastVerifyError = json?.data?.description || '验证码验证失败';
pendingVerify = true; // 继续挂验证码状态
console.log(`❌ [验证码错误] ${lastVerifyError}`);
} else {
lastVerifyError = '';
}
} catch (e) {
console.error('❌ validate_code 解析失败', e);
}
}
if(url.includes("support/user/info")){
console.log('📡 url:', url);
const text = await response.text();
console.log('📡 原始响应:', text);
console.log(`✅ [扫码成功] 准备保存账号【${accountName}】的 Cookie`);
await saveCookies(page, accountName);
const filename = `img/scree_${getTimestamp()}.png`;
await page.screenshot({ path: filename });
console.log(`📸 扫码成功 已保存为 ${filename}`);
// const text = await response.text();
const json = JSON.parse(text);
userInfo = json; // ✅ 保存
console.log(`✅ 已保存用户信息: ${json.basic?.nickname || ''}`);
}
});
// 未自动登录,挂监听:检测跳转成功就保存
page.on('framenavigated', async (frame) => {
console.log('当前 Frame URL:', frame.url());
if (frame.url().includes('/douyin-mp/home')) {
// console.log(`✅ [扫码成功] 准备保存账号【${accountName}】的 Cookie`);
// await saveCookies(page, accountName);
}
});
try {
console.log(`🟡 正在打开入口页面:${entryUrl}`);
const res = await page.goto(entryUrl, {
waitUntil: 'networkidle2',
timeout: 15000,
});
if (!res || !res.ok()) {
throw new Error(`页面加载失败: ${res?.status()}`);
}
// await page.waitForTimeout(8000); // 页面可能异步加载二维码请求
// await page.screenshot({ path: 'success.png' });
// console.log("📸 页面截图已保存为 success.png");
} catch (err) {
console.error("❌ 页面加载出错:", err.message);
await page.screenshot({ path: 'error_screenshot.png' });
console.log("📸 错误截图已保存为 error_screenshot.png");
}
//await browser.close();
return hookResult;
}
// module.exports = fetchQrFromPage;
// module.exports = {
// fetchQrFromPage,
// pendingVerify: () => pendingVerify,
// submitVerifyCode,
// qrCodeContent: () => qrCodeContent,
// userInfo: () => userInfo
// };
module.exports = {
stopBot, // ✅ 暴露
fetchQrFromPage,
pendingVerify: () => pendingVerify,
submitVerifyCode,
qrCodeContent: () => qrCodeContent,
userInfo: () => userInfo,
scanStatus: () => scanStatus,
scanUser: () => scanUser,
verifyError: () => lastVerifyError // ✅ 新增
};
console.log("欢迎来到 InsCode");
\ No newline at end of file
{ {
"name": "nodejs", "name": "debot-fetch",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "Headless Puppeteer scraper for debot.ai",
"main": "index.js", "main": "run.js",
"scripts": { "scripts": {
"dev": "node index.js", "start": "node run.js"
"test": "echo \"Error: no test specified\" && exit 1" },
}, "dependencies": {
"keywords": [], "express": "^5.1.0",
"author": "", "puppeteer": "^21.3.8",
"license": "ISC", "puppeteer-extra": "^3.3.6",
"dependencies": { "puppeteer-extra-plugin-stealth": "^2.11.1"
"@types/node": "^18.0.6",
"node-fetch": "^3.2.6"
}
} }
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>抖音扫码登录状态页</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
#qrcode-section, #scanned-section, #verify-section, #logged-section,#account1,#start-btn1 { display: none; margin-bottom: 20px; }
img { max-width: 200px; }
#avatar { border-radius: 50%; }
input { padding: 5px; }
button { padding: 5px 10px; }
.status { font-weight: bold; color: #0077cc; }
</style>
</head>
<body>
<h1>扫码登录</h1>
<!-- 启动按钮 -->
<input id="account" placeholder="输入账户名">
<button id="start-btn">启动扫码</button>
<!-- 停止按钮(可选) -->
<button id="stop-btn">停止运行</button>
<!-- 新状态提示 -->
<div id="qrcode-status">当前状态: <span id="scan-status" class="status">加载中...</span></div>
<!-- 一、二维码状态 -->
<div id="qrcode-section">
<h2>请使用抖音 APP 扫码</h2>
<img id="qrcode" alt="等待生成二维码..." />
</div>
<img id="avatar" alt="头像" />
<!-- 二、已扫码状态 -->
<div id="scanned-section">
<h2>已扫码,请在手机确认</h2>
<p> <span id="scanned-nickname"></span></p>
</div>
<!-- 三、需要验证码状态 -->
<div id="verify-section">
<h2>请输入短信验证码</h2>
<div id="verify-error" style="color:red;"></div>
<input id="code" placeholder="输入验证码" />
<button id="submit">提交验证码</button>
</div>
<!-- 四、登录成功状态 -->
<div id="logged-section">
<h2>🎉 登录成功</h2>
<p>昵称: <span id="nickname"></span></p>
<img id="logged-avatar" alt="头像" />
</div>
<script>
document.getElementById('start-btn').onclick = async () => {
const account = document.getElementById('account').value.trim();
if (!account) return alert('请先输入账户名');
const res = await fetch('/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ account })
}).then(r => r.json());
alert(res.success ? res.msg : res.msg);
};
document.getElementById('stop-btn').onclick = async () => {
const res = await fetch('/stop', {
method: 'POST'
}).then(r => r.json());
alert(res.success ? res.msg : res.msg);
};
</script>
<script>
async function pollStatus() {
const res = await fetch('/status').then(r => r.json());
// 更新状态文本
document.getElementById('scan-status').innerText = res.scanStatus || '等待启动';
document.getElementById('verify-error').innerText = res.verifyError || '';
// 重置所有区域显示
document.getElementById('qrcode-section').style.display = 'none';
document.getElementById('scanned-section').style.display = 'none';
document.getElementById('verify-section').style.display = 'none';
document.getElementById('logged-section').style.display = 'none';
document.getElementById('avatar').style.display = 'none';
document.getElementById('qrcode-status').style.display = 'none';
// 如果已登录
if (res.user?.basic?.nickname) {
document.getElementById('logged-section').style.display = 'block';
document.getElementById('nickname').innerText = res.user.basic.nickname;
const avatarUrl = res.user.basic?.avatar?.url_list?.[0];
if (avatarUrl) {
document.getElementById('logged-avatar').src = avatarUrl;
}
return; // 已登录直接返回
}
document.getElementById('qrcode-status').style.display = 'block';
//显示头像
if (res.scanUser?.avatar_url) {
document.getElementById('avatar').style.display = 'block';
document.getElementById('avatar').src = res.scanUser.avatar_url;
}
// 否则根据状态显示
if (res.scanStatus === 'new') {
document.getElementById('qrcode-section').style.display = 'block';
if (res.qrcode) {
document.getElementById('qrcode').src = 'data:image/png;base64,' + res.qrcode;
}
} else if (res.scanStatus === 'scanned') {
document.getElementById('scanned-section').style.display = 'block';
if (res.scanUser?.avatar_url) {
document.getElementById('avatar').src = res.scanUser.avatar_url;
}
document.getElementById('scanned-nickname').innerText = res.scanUser?.screen_name || '';
} else if (res.scanStatus === 'verify' && res.pendingVerify) {
document.getElementById('verify-section').style.display = 'block';
}
}
// 提交验证码
document.getElementById('submit').onclick = async () => {
const code = document.getElementById('code').value.trim();
if (!code) {
alert('请输入验证码');
return;
}
const res = await fetch('/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
}).then(r => r.json());
alert(res.success ? '✅ 验证码已提交!' : '❌ 提交失败');
};
// 定时轮询状态
setInterval(pollStatus, 2000);
pollStatus();
</script>
</body>
</html>
// run.js
const core = require('./fetchWithPuppeteer.js');
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.static('public'));
let currentBot = null; // ✅ 当前 Puppeteer 是否已在运行
let currentAccount = null;
app.post('/start', async (req, res) => {
const { account } = req.body;
if (!account) return res.json({ success: false, msg: '缺少账户名' });
if (currentBot) {
return res.json({ success: false, msg: `已有实例在运行: ${currentAccount}` });
}
console.log(`🚀 [启动] 启动 Puppeteer 账户:${account}`);
currentAccount = account;
currentBot = core.fetchQrFromPage(
"https://e.douyin.com/site/douyin-mp/operation-center/im-manage/imchat",
account
).then(() => {
console.log(`✅ [结束] 账户:${account} 扫码流程结束`);
currentBot = null; // 自动释放锁
currentAccount = null;
});
res.json({ success: true, msg: `已启动账户:${account}` });
});
// 可选:提供停止接口(手动关闭 Puppeteer)
app.post('/stop', async (_, res) => {
if (!currentBot) return res.json({ success: false, msg: '没有运行中的实例' });
// ⚠️ 要实现 core.stopBot() 关闭 Puppeteer browser
await core.stopBot();
currentBot = null;
currentAccount = null;
res.json({ success: true, msg: '已停止' });
});
// app.get('/status', (_, res) => {
// res.json({ pendingVerify: core.pendingVerify() });
// });
app.get('/status', (_, res) => {
res.json({
pendingVerify: core.pendingVerify(),
qrcode: core.qrCodeContent(),
user: core.userInfo(),
scanStatus: core.scanStatus(),
scanUser: core.scanUser(),
verifyError: core.verifyError() // ✅ 新增
});
});
app.post('/verify', async (req, res) => {
const { code } = req.body;
await core.submitVerifyCode(code);
res.json({ success: true });
});
app.get('/qrcode', (_, res) => {
res.json({ qrcode: core.qrCodeContent() });
});
app.get('/user', (_, res) => {
res.json({ user: core.userInfo() });
});
app.listen(3000, () => {
console.log('🌐 验证码输入服务已启动:http://localhost:3000');
});
// const entry = "https://e.douyin.com/site/douyin-mp/operation-center/im-manage/imchat";
// core.fetchQrFromPage(entry,"user2").then(res => {
// console.log("运行浏览器结束", res);
// });
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册