You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

本地正常的WhatsApp批量消息API部署至线上服务器异常求助

问题描述

我开发了一款可通过CSV文件批量发送WhatsApp消息与媒体文件的API,本地服务器运行正常,但部署至Render等线上服务器时,Chromium浏览器及核心代码出现异常,无任何执行迹象。以下是初始代码及修改后的代码,请求排查部署后的异常问题。

初始代码

const express = require('express');
const multer = require('multer');
const { promisify } = require('util');
const fs = require('fs');
const bodyParser = require('body-parser');
const { Client, LocalAuth } = require('whatsapp-web.js');
const { error } = require('console');
const { Session } = require('inspector');
const { MessageMedia } = require('whatsapp-web.js');
const { forEach } = require('lodash');
const cors = require('cors');

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors())
const storage = multer.diskStorage({
    destination: (req, file, cb)=>{
      cb(null,'Csv/');
    },
    filename:(req, file, cb)=>{
      cb(null, file.originalname);
    }
  });

  const Csv = multer({
    storage: storage
  })



  const Storage = multer.diskStorage({
    destination: (req, file, cb) => {
      cb(null, './uploads'); // The directory where uploaded files will be saved
    },
    filename: (req, file, cb) => {
      cb(null, file.originalname); // Use the original file name
    },
  });
  const upload = multer({ storage: Storage });

// Initialize the WhatsApp client



app.get('/send', (req, res) => {
    const client = new Client({
    authStrategy: new LocalAuth(),
    // Options for the client
      puppeteer: {
          headless: false,
          args:[
              '--no-sandbox',
              '--disable-setuid-sandbox'
              ]
              }
  });
  
  client.on('qr', (qr) => {
   // Generate and display the QR code in the terminal
   console.log('QR Code:', qr);
  });
  
  client.on('ready', () => {
   console.log('Client is ready!');
  });
  
  client.initialize();
})


app.post('/send-message', Csv.single('numbers'), async (req, res) => {
  
    try {
      const message = req.body.message;
        const numbers = await promisify(fs.readFile)(req.file.path);
        console.log(message);
        console.log(numbers);
        let number = numbers.toString().split('\r\n');
        for (let index = 0; index < number.length; index++) {
          let test = number[index]
          console.log(test);
          let code = await client.sendMessage('234' + test + '@c.us', message);
          console.log(code);
        }
    
        // Send the message to each number
    //         console.log('Message sent to:', number);
    //         
    
        res.status(200).send('Messages sent successfully!');
      } catch (error) {
        console.error('Error:', error);
         res.status(500).send('Failed to send messages!');
      }
});


app.post('/send-media', upload.fields([{ name: 'media', maxCount: 1 }, { name: 'csv', maxCount: 1 }]), async(req, res) => {
  const csvFile = await promisify(fs.readFile)(req.files['csv'][0].path);
  const mediaFile = req.files['media'][0];
  const message = req.body.message;
  console.log(mediaFile);
  console.log(message);
  try{

  let number = csvFile.toString().split('\r\n');
  console.log(number);
  for (let index = 0; index < number.length; index++) {
    let csv = number[index]
      const chat = await client.getChatById('234' + csv + '@c.us');
      console.log(chat);
      const media = MessageMedia.fromFilePath(mediaFile.path);
      chat.sendMessage(media, { caption: message });
  };
  res.status(200).json({ success: true, message: 'Media sent' })
} catch (error) {
  res.status(500).json({ success: false, error: 'Failed to send media' });
}
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Server is running on port ${port}`));

修改后的代码

const express = require('express');
const multer = require('multer');
const { promisify } = require('util');
const fs = require('fs');
const bodyParser = require('body-parser');
const puppeteer = require('puppeteer');
const { Client, LocalAuth } = require('whatsapp-web.js');
const { error } = require('console');
const { Session } = require('inspector');
const { MessageMedia } = require('whatsapp-web.js');
const { forEach } = require('lodash');
const cors = require('cors');
const qrcode = require('qrcode-terminal');

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors())
const storage = multer.diskStorage({
    destination: (req, file, cb)=>{
      cb(null,'Csv/');
    },
    filename:(req, file, cb)=>{
      cb(null, file.originalname);
    }
  });

  const Csv = multer({
    storage: storage
  })



  const Storage = multer.diskStorage({
    destination: (req, file, cb) => {
      cb(null, './uploads'); // The directory where uploaded files will be saved
    },
    filename: (req, file, cb) => {
      cb(null, file.originalname); // Use the original file name
    },
  });
  const upload = multer({ storage: Storage });

// Initialize the WhatsApp client



const client = new Client({
  puppeteer: {
    headless: false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ],
  } 
  // Options for the client
});

client.on('qr', (qr) => {
 // Generate and display the QR code in the terminal
 console.log('QR Code:', qr);
});

client.on('ready', () => {
 console.log('Client is ready!');
});

client.initialize();


app.post('/send-message', Csv.single('numbers'), async (req, res) => {
  
    try {
      const message = req.body.message;
        const numbers = await promisify(fs.readFile)(req.file.path);
        console.log(message);
        console.log(numbers);
        let number = numbers.toString().split('\r\n');
        for (let index = 0; index < number.length; index++) {
          let test = number[index]
          console.log(test);
          let code = await client.sendMessage('234' + test + '@c.us', message);
          console.log(code);
        }
    
        // Send the message to each number
    //         console.log('Message sent to:', number);
    //         
    
        res.status(200).send('Messages sent successfully!');
      } catch (error) {
        console.error('Error:', error);
         res.status(500).send('Failed to send messages!');
      }
});


app.post('/send-media', upload.fields([{ name: 'media', maxCount: 1 }, { name: 'csv', maxCount: 1 }]), async(req, res) => {
  const csvFile = await promisify(fs.readFile)(req.files['csv'][0].path);
  const mediaFile = req.files['media'][0];
  const message = req.body.message;
  console.log(mediaFile);
  console.log(message);
  try{

  let number = csvFile.toString().split('\r\n');
  console.log(number);
  for (let index = 0; index < number.length; index++) {
    let csv = number[index]
      const chat = await client.getChatById('234' + csv + '@c.us');
      console.log(chat);
      const media = MessageMedia.fromFilePath(mediaFile.path);
      chat.sendMessage(media, { caption: message });
  };
  res.status(200).json({ success: true, message: 'Media sent' })
} catch (error) {
  res.status(500).json({ success: false, error: 'Failed to send media' });
}
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Server is running on port ${port}`));
问题排查与解决方案

核心问题分析

线上服务器无图形界面、资源受限,与本地环境差异导致Chromium启动失败或核心逻辑无法执行,具体原因包括:

  • Headless模式错误:线上无图形界面,headless: false会导致Chromium无法启动,必须使用无头模式。
  • Puppeteer启动参数不足:仅--no-sandbox和--disable-setuid-sandbox不足以适配线上环境,缺少GPU禁用、共享内存优化等参数。
  • 会话持久化失败:LocalAuth依赖本地文件系统存储会话,线上服务器文件系统多为临时存储,重启后会话丢失,且可能无写入权限。
  • 上传目录未创建:multer指定的Csv/和./uploads目录在服务器上可能不存在,导致文件无法保存。
  • 系统依赖缺失:线上服务器默认缺少Chromium运行所需的系统库(如libnss3、libatk1.0-0等)。

具体修复步骤

1. 修正Puppeteer配置

将无头模式设为新版'new',补充必要启动参数:

const client = new Client({
  authStrategy: new LocalAuth(),
  puppeteer: {
    headless: 'new', // 新版无头模式,兼容性更好
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-gpu', // 禁用GPU,适配无图形环境
      '--disable-dev-shm-usage', // 避免共享内存不足
      '--single-process', // 单进程模式,减少资源占用
      '--no-zygote' // 禁用zygote进程,避免权限问题
    ]
  }
});

2. 确保上传目录存在

启动时自动创建所需目录,避免文件保存失败:

// 在初始化app后添加
const ensureDir = (dirPath) => {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true }); // 递归创建目录
  }
};
ensureDir('./Csv');
ensureDir('./uploads');

3. 增强错误日志捕捉

添加客户端错误事件监听,获取Chromium启动失败的具体信息:

client.on('error', (err) => {
  console.error('WhatsApp Client 错误:', err);
});
client.on('disconnected', (reason) => {
  console.log('客户端断开连接:', reason);
  // 可选:自动重连
  client.initialize();
});

4. 检查客户端就绪状态

在接口中确保客户端已就绪再处理请求,避免未初始化时调用发送逻辑:

// 全局标记客户端状态
let isClientReady = false;
client.on('ready', () => {
  console.log('Client is ready!');
  isClientReady = true;
});

// 修改send-message接口
app.post('/send-message', Csv.single('numbers'), async (req, res) => {
  if (!isClientReady) {
    return res.status(503).send('客户端未就绪,请稍后重试');
  }
  // 原有逻辑...
});

5. 安装系统依赖

在Render等平台的构建命令中添加系统依赖安装步骤(以Ubuntu为例):

sudo apt-get update && sudo apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2

6. 优化会话持久化(可选)

线上环境建议改用RemoteAuth将会话存储到Redis等持久化存储,避免依赖本地文件:

// 示例:使用Redis存储会话
const { RemoteAuth } = require('whatsapp-web.js');
const Redis = require('ioredis');

const redisClient = new Redis(process.env.REDIS_URL);

const client = new Client({
  authStrategy: new RemoteAuth({
    clientId: 'your-client-id',
    store: redisClient,
    backupSyncIntervalMs: 300000
  }),
  // puppeteer配置...
});

内容的提问来源于stack exchange,提问作者John

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 18:45:53