whatsapp-auth.js 7.82 KB
/**
 * WhatsApp Auth Route — QR 码认证 API
 *
 * GET  /api/whatsapp/status   → 获取 WhatsApp 连接状态
 * GET  /api/whatsapp/auth     → SSE: 启动认证流程,推送 QR 码
 * POST /api/whatsapp/logout   → 登出并清除凭据
 */
const express = require('express');
const path = require('path');
const fs = require('fs');
const router = express.Router();
const { requireAuth } = require('../middleware/jwt-auth');

// 38 号文档 Phase 1:这是本机管理员在浏览器里扫码连接 WhatsApp bot 的操作面板,
// 不是外部 WhatsApp 服务器回调的 webhook,挂登录门槛不会挡外部回调。
router.use(requireAuth);

const AUTH_DIR = path.resolve(__dirname, '../../skills/whatsapp-bot/store/auth');

// ─── 状态查询 ───

router.get('/status', (req, res) => {
    const credsPath = path.join(AUTH_DIR, 'creds.json');
    let authenticated = false;
    if (fs.existsSync(credsPath)) {
        try {
            const creds = JSON.parse(fs.readFileSync(credsPath, 'utf-8'));
            // Baileys v7 rc.9 有时 registered 不会变为 true,
            // 但如果 auth 目录有大量文件 (session/key files),说明认证已完成
            const authFiles = fs.readdirSync(AUTH_DIR).length;
            authenticated = creds.registered === true || authFiles > 5;
        } catch {
            authenticated = false;
        }
    }
    res.json({
        authenticated,
        authDir: AUTH_DIR,
    });
});

// ─── SSE 认证流程 ───

router.get('/auth', async (req, res) => {
    // SSE headers
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        Connection: 'keep-alive',
    });

    const sendSSE = (event, data) => {
        res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
    };

    sendSSE('status', { message: '正在初始化 WhatsApp 连接...' });

    let sock = null;
    let clientClosed = false;
    let authDone = false;
    let retryCount = 0;
    const MAX_RETRIES = 5;

    // 客户端断开时清理
    req.on('close', () => {
        clientClosed = true;
        if (sock) {
            try { sock.end(undefined); } catch { }
            sock = null;
        }
    });

    try {
        const {
            default: makeWASocket,
            useMultiFileAuthState,
            fetchLatestWaWebVersion,
            makeCacheableSignalKeyStore,
            Browsers,
            DisconnectReason,
        } = await import('@whiskeysockets/baileys');

        fs.mkdirSync(AUTH_DIR, { recursive: true });

        // 半静默 logger:仅输出 error/warn
        const silentLogger = {
            info: () => { },
            error: (...a) => console.error('[WhatsApp Auth][baileys]', ...a),
            warn: (...a) => console.warn('[WhatsApp Auth][baileys]', ...a),
            debug: () => { },
            trace: () => { },
            child: () => silentLogger,
            level: 'error',
        };

        let version;
        try {
            const result = await fetchLatestWaWebVersion({});
            version = result.version;
        } catch {
            sendSSE('status', { message: '无法获取最新版本号,使用默认版本' });
        }

        // 可重连的 socket 创建函数 (处理 code 515 restart required)
        async function connectSocket() {
            if (clientClosed) return;

            const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);

            sock = makeWASocket({
                version,
                auth: {
                    creds: state.creds,
                    keys: makeCacheableSignalKeyStore(state.keys, silentLogger),
                },
                printQRInTerminal: false,
                browser: Browsers.macOS('Chrome'),
                logger: silentLogger,
            });

            sock.ev.on('connection.update', (update) => {
                const { connection, lastDisconnect, qr } = update;
                console.log('[WhatsApp Auth] connection.update:', JSON.stringify({ connection, qr: !!qr, lastDisconnect: lastDisconnect?.error?.message }));

                if (qr) {
                    sendSSE('qr', { qrCode: qr });
                }

                if (connection === 'close') {
                    if (authDone) {
                        // 认证已完成后的断开是预期行为 (sock.end)
                        console.log('[WhatsApp Auth] Post-auth disconnect, ignoring.');
                        return;
                    }
                    const reason = lastDisconnect?.error?.output?.statusCode;
                    console.log('[WhatsApp Auth] Connection closed, reason:', reason);

                    if (reason === DisconnectReason.loggedOut) {
                        sendSSE('error', { message: '认证失败: 已被登出。请重试。' });
                        if (!clientClosed) res.end();
                    } else if (reason === DisconnectReason.restartRequired || reason === 515) {
                        // WhatsApp 要求重启流 — QR 扫码后的正常行为
                        retryCount++;
                        console.log(`[WhatsApp Auth] Restart required, reconnecting... (${retryCount}/${MAX_RETRIES})`);
                        if (retryCount <= MAX_RETRIES && !clientClosed) {
                            sendSSE('status', { message: '正在完成认证...' });
                            setTimeout(() => connectSocket(), 1000);
                        } else {
                            sendSSE('error', { message: '重连次数过多,请刷新重试' });
                            if (!clientClosed) res.end();
                        }
                    } else {
                        sendSSE('status', { message: `连接断开 (${reason}),等待重试...` });
                        // 对其他断线原因也尝试重连
                        retryCount++;
                        if (retryCount <= MAX_RETRIES && !clientClosed) {
                            setTimeout(() => connectSocket(), 2000);
                        } else {
                            sendSSE('error', { message: '重连失败,请刷新重试' });
                            if (!clientClosed) res.end();
                        }
                    }
                } else if (connection === 'open') {
                    authDone = true;
                    // 认证完成后静默 logger,避免 sock.end() 后 Baileys 内部错误刷屏
                    silentLogger.error = () => { };
                    silentLogger.warn = () => { };
                    const phoneNumber = sock.user?.id?.split(':')[0]?.split('@')[0];
                    console.log('[WhatsApp Auth] ✅ Authenticated! Phone:', phoneNumber);
                    sendSSE('authenticated', {
                        message: `认证成功! 手机号: ${phoneNumber || '未知'}`,
                        phone: phoneNumber,
                    });

                    // 认证成功后等待 creds 完全写入再断开(Bot 单独运行)
                    setTimeout(() => {
                        try { sock.end(undefined); } catch { }
                        sock = null;
                        if (!clientClosed) res.end();
                    }, 5000);
                }
            });

            sock.ev.on('creds.update', saveCreds);
        }

        await connectSocket();

    } catch (err) {
        console.error('[WhatsApp Auth] Error:', err);
        sendSSE('error', { message: err.message });
        if (!clientClosed) res.end();
    }
});

// ─── 登出 ───

router.post('/logout', (req, res) => {
    try {
        if (fs.existsSync(AUTH_DIR)) {
            fs.rmSync(AUTH_DIR, { recursive: true, force: true });
        }
        res.json({ success: true, message: 'WhatsApp 凭据已清除' });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

module.exports = router;