如何用Express+Puppeteer通过Webhooks实现Tradovate交易按钮自动点击?
问题:Webhook触发的Tradovate自动化交易无响应
我正在做一个项目,通过模拟点击按钮,把Webhook传来的交易信号自动同步到Tradovate平台。预期流程是:
- 运行JS应用
- 打开Chrome并登录Tradovate账户
- 控制台打印“Awaiting...”和“Node.js server started on port 5000.”
- 用Insomnia模拟Webhook请求
- 向Ngrok转发的URL发送POST请求(JSON格式),返回200 OK
- 根据
order_action的值,自动点击买入或卖出按钮
现在Ngrok已经在转发5000端口,但买入/卖出操作完全没触发,也没有报错。我怀疑问题出在app.post( '/', async function( req, res )这部分代码,因为我对Puppeteer不太熟,希望能得到帮助。
相关代码如下:
import puppeteer from 'puppeteer'; import express from 'express'; const app = express(); app.use( express.json() ); (async () => { // Launch the browser and open a new blank page const browser = await puppeteer.launch( {headless: false} ); const page = await browser.newPage(); // Navigate the page to a URL await page.goto('https://trader.tradovate.com/welcome'); // Set screen size await page.setViewport({width: 1080, height: 1024}); // Type into search box await page.type('#name-input', 'XXXXX'); await page.type('#password-input', 'XXXXX'); const login = '.MuiButtonBase-root.MuiButton-root'; //const login = '#onboarding-view-div > div.MuiContainer-root.onboarding-content.main-container.MuiContainer-maxWidthLg > div > div.mb-60 > div > div:nth-child(1) > div > form > button'; await page.waitForSelector(login); await page.click(login); //await page.keyboard.press('Enter'); await page.waitForNavigation({ waitUntil: "networkidle0", }) await page.click("#tm_options > div:nth-child(2) > div > div:nth-child(2) > button"); console.log( 'Awaiting...' ) app.post( '/', async function( req, res ) { if (req.body.strategy.order_action == 'buy') { await page.click("#content > div > div.app-modules > div > div.gm-scroll-view > div > div > div > div:nth-child(1) > div:nth-child(1) > div.lm_items > div > div > div > div.module.chart.chart-wrapper > div.header > div > div > div.gm-scroll-view > div:nth-child(6) > div:nth-child(2) > div:nth-child(1) > div") await page.click("#none > div.popover-content > div.btn-group-justified.btn-group > div:nth-child(1) > div") console.log('Buy initiated!') } else if (req.body.strategy.order_action == 'sell') { await page.click("#content > div > div.app-modules > div > div.gm-scroll-view > div > div > div > div:nth-child(1) > div:nth-child(1) > div.lm_items > div > div > div > div.module.chart.chart-wrapper > div.header > div > div > div.gm-scroll-view > div:nth-child(6) > div:nth-child(2) > div:nth-child(2) > div") await page.click( "#none > div.popover-content > div.btn-group-justified.btn-group > div:nth-child(1) > div" ) console.log('Sell initiated!') } res.sendStatus( 200 ); } ); app.listen( 5000, () => console.log( 'Node.js server started on port 5000.' ) ); })();
可能的问题及解决方法
1. 路由注册时机不合理
你把app.post路由注册放在了Puppeteer的异步启动函数内部,虽然能运行,但容易导致上下文混乱,也不符合Express的常规写法。建议把路由注册移到外层,只将page变量暴露给路由处理函数。
2. DOM选择器过于脆弱
你使用的是超长的绝对路径选择器,只要页面结构有微小变动(比如元素位置调整、类名修改)就会失效。解决办法:
- 优先使用平台提供的语义化选择器,比如元素的
data-*属性、特定功能类名 - 在点击前先等待选择器加载完成,避免元素未就绪就执行操作
3. 异步操作未做错误捕获
路由里的Puppeteer操作是异步的,但如果没有错误捕获逻辑,报错会被直接吞掉,导致你看不到问题所在。必须加上try/catch块来捕获并打印错误。
4. 请求体解析验证
虽然你用了app.use(express.json()),但要确保Insomnia发送请求时的Content-Type头是application/json,否则req.body会是undefined,导致判断条件不成立。可以在路由开头打印req.body确认数据是否正确接收。
修改后的完整代码示例
import puppeteer from 'puppeteer'; import express from 'express'; const app = express(); app.use(express.json()); let page; // 外层定义page变量,供路由调用 // 初始化Puppeteer (async () => { const browser = await puppeteer.launch({ headless: false }); page = await browser.newPage(); await page.setViewport({ width: 1080, height: 1024 }); await page.goto('https://trader.tradovate.com/welcome'); // 登录操作 await page.type('#name-input', 'XXXXX'); await page.type('#password-input', 'XXXXX'); const loginSelector = '.MuiButtonBase-root.MuiButton-root'; await page.waitForSelector(loginSelector); await page.click(loginSelector); await page.waitForNavigation({ waitUntil: "networkidle0" }); await page.click("#tm_options > div:nth-child(2) > div > div:nth-child(2) > button"); console.log('Awaiting...'); })(); // 注册Webhook路由 app.post('/', async function(req, res) { try { console.log('收到请求数据:', req.body); // 检查Puppeteer页面是否初始化完成 if (!page) { console.error('Puppeteer页面未初始化'); return res.sendStatus(503); } const orderAction = req.body.strategy?.order_action; if (!orderAction) { console.error('请求中缺少order_action字段'); return res.sendStatus(400); } if (orderAction === 'buy') { // 等待买入按钮加载完成再点击 const buyBtnSelector = "#content > div > div.app-modules > div > div.gm-scroll-view > div > div > div > div:nth-child(1) > div:nth-child(1) > div.lm_items > div > div > div > div.module.chart.chart-wrapper > div.header > div > div > div.gm-scroll-view > div:nth-child(6) > div:nth-child(2) > div:nth-child(1) > div"; await page.waitForSelector(buyBtnSelector, { timeout: 5000 }); await page.click(buyBtnSelector); // 等待确认按钮加载完成 const confirmBuySelector = "#none > div.popover-content > div.btn-group-justified.btn-group > div:nth-child(1) > div"; await page.waitForSelector(confirmBuySelector, { timeout: 5000 }); await page.click(confirmBuySelector); console.log('Buy initiated!'); } else if (orderAction === 'sell') { const sellBtnSelector = "#content > div > div.app-modules > div > div.gm-scroll-view > div > div > div > div:nth-child(1) > div:nth-child(1) > div.lm_items > div > div > div > div.module.chart.chart-wrapper > div.header > div > div > div.gm-scroll-view > div:nth-child(6) > div:nth-child(2) > div:nth-child(2) > div"; await page.waitForSelector(sellBtnSelector, { timeout: 5000 }); await page.click(sellBtnSelector); const confirmSellSelector = "#none > div.popover-content > div.btn-group-justified.btn-group > div:nth-child(1) > div"; await page.waitForSelector(confirmSellSelector, { timeout: 5000 }); await page.click(confirmSellSelector); console.log('Sell initiated!'); } else { console.error('未知的order_action:', orderAction); return res.sendStatus(400); } res.sendStatus(200); } catch (err) { console.error('操作执行失败:', err); res.sendStatus(500); } }); app.listen(5000, () => console.log('Node.js server started on port 5000.'));
内容的提问来源于stack exchange,提问作者IscA
相关产品推荐
相关产品推荐

