whatsapp-auth.js
7.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/**
* 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;