Files
gavindiaz 3c3899a115 Add pitfall #7 (recommendation format bug) + diagnostic script reference
After 8 hours of silent signal swallowing, document:
- Root cause: watcher expected "ENTER:" prefix but bot writes "side:phase:strength"
- Discovery method: analyze.sh recommendation distribution shows signals vs pushes
- Fix commit reference: 08560d0
- Three concrete lessons for cross-process data contracts

Also:
- Reference analyze.sh in cheat sheet (信号诊断 command)
- Fix manual test format in cheat sheet (no more ENTER: prefix)
- Add file structure entry for analyze.sh
- Add security checklist item: check real field formats before editing parsers
2026-07-22 02:21:02 +08:00

16 KiB
Raw Permalink Blame History

云端部署指南

本文基于实际部署流程编写,记录从代码拉取到稳定运行的完整步骤,以及过程中踩过的所有坑。


一、架构概览

云端部署两个 PM2 进程:

进程 作用 端口/资源
polymarket-bot 主程序,仪表盘渲染 + 计算 + 写 logs/signals.csv ~100MB 内存,每 1.5s 一帧
polymarket-watcher 监听 CSV,推送 ENTER 信号到 Telegram ~50MB 内存,事件驱动

两者互相独立——watcher 不依赖 bot 的进程健康,bot 重启不影响 watcher 持续监听。

数据流:

[Polymarket API] → polymarket-bot → logs/signals.csv → polymarket-watcher → [Telegram API]

二、前置条件

2.1 Telegram Bot 准备(部署前必做

任何在对话/聊天记录里出现过的 token 都视为已泄露,必须换新:

  1. Telegram 里找 @BotFather
  2. /revoke → 选你的 bot → 撤销旧 token
  3. /token → 选同一个 bot → 拿到新 token
  4. 给新 bot 发任意消息(/start
  5. 浏览器访问(需挂代理):
    https://api.telegram.org/bot<新TOKEN>/getUpdates
    
    "chat":{"id":XXXXX,...} → 这才是你的 chat_id

⚠️ 新 token 不能发给我或进 git

2.2 服务器要求

  • LinuxUbuntu 22.04+ 测试通过)
  • Node.js 18+(推荐 20.x
  • systemd(用于 PM2 开机自启)
  • 内存 ≥ 512MB
  • 国内云服务器需要代理访问 Telegram/Polymarket

三、一次性部署流程

3.1 服务器基础环境

# Node.js 20.x
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs git

# PM2 全局
sudo npm install -g pm2

# 验证
node --version    # v20.x.x
pm2 --version

3.2 拉取代码

cd /opt
sudo git clone https://github.com/FrondEnt/PolymarketBTC15mAssistant.git
# 或你自己的 fork
# sudo git clone https://code.aifunny.ltd/你的用户名/PolymarketBTC15mAssistant.git
sudo chown -R $USER:$USER PolymarketBTC15mAssistant
cd PolymarketBTC15mAssistant
npm install

3.3 创建 .env(密钥文件)

项目不读 .env,但 start.sh 会 source 它把变量注入 Node 进程。

cp .env.example .env
nano .env

填入实际值:

TELEGRAM_BOT_TOKEN=<新token>
TELEGRAM_CHAT_ID=<你的chat_id>
TELEGRAM_COOLDOWN_MS=30000

POLYGON_RPC_URL=https://lb.drpc.live/polygon/your-key
POLYGON_WSS_URLS=wss://lb.drpc.live/polygon/your-key

# HTTPS_PROXY=http://user:pass@ip:7890   # 国内云服务器需要

收紧权限:

chmod 600 .env

3.4 创建 start.shenv wrapper

这个文件不在 git 里(gitignore 了),服务器要现场写:

cat > start.sh <<'EOF'
#!/bin/bash
set -a
source "$(dirname "$0")/.env"
set +a
exec node "$@"
EOF
chmod +x start.sh

作用:先 source .env 把变量导出,然后 exec 替换当前 shell 跑 node,子进程继承所有 env vars。

3.5 创建 ecosystem.config.cjsPM2 配置)

同样 gitignore,路径改成 /opt/...

cat > ecosystem.config.cjs <<'EOF'
module.exports = {
  apps: [
    {
      name: "polymarket-bot",
      script: "./start.sh",
      args: "src/index.js",
      cwd: "/opt/PolymarketBTC15mAssistant",
      max_memory_restart: "500M",
      out_file: "./logs/pm2-bot.out.log",
      error_file: "./logs/pm2-bot.err.log",
      merge_logs: true
    },
    {
      name: "polymarket-watcher",
      script: "./start.sh",
      args: "scripts/telegram-watcher.js",
      cwd: "/opt/PolymarketBTC15mAssistant",
      max_memory_restart: "200M",
      out_file: "./logs/pm2-watcher.out.log",
      error_file: "./logs/pm2-watcher.err.log",
      merge_logs: true
    }
  ]
};
EOF

3.6 启动 + 开机自启

# 启动
pm2 start ecosystem.config.cjs

# 开机自启(复制输出的 sudo 命令执行)
pm2 startup
# 例:sudo env PATH=... systemctl enable pm2-root

# 保存当前进程列表(开机时按此恢复)
pm2 save

⚠️ pm2 startuppm2 save 两个都要做

  • startup 生成 systemd unit 让 PM2 守护进程开机启动
  • save 保存当前进程列表,让 PM2 启动时知道要 resurrect 哪些 app

四、常用操作速查

4.1 状态查看

pm2 status                  # 所有进程概览
pm2 monit                   # 实时 CPU/内存监控(curses 界面)
pm2 list                    # 同 status 但更紧凑
pm2 describe polymarket-bot # 单个进程详细信息
pm2 env polymarket-watcher  # 进程实际 env vars

4.2 启停重启

pm2 start ecosystem.config.cjs           # 启动所有
pm2 start src/index.js --name polymarket-bot   # 临时启动

pm2 stop polymarket-bot                  # 停止单个
pm2 stop all                            # 停止所有

pm2 restart polymarket-watcher          # 重启单个
pm2 restart all                         # 重启所有

pm2 reload all                          # 0秒停机重启(cluster 模式才有用)

pm2 delete polymarket-bot               # 从 PM2 移除
pm2 delete all                          # 清空
pm2 kill                                # 杀 PM2 守护进程(极端情况)

4.3 日志查看

pm2 logs                                  # 所有进程实时日志(Ctrl+C 退出)
pm2 logs polymarket-bot                   # 单个进程
pm2 logs polymarket-bot --lines 100       # 最近 100 行
pm2 logs polymarket-watcher --err         # 只看错误
pm2 logs polymarket-watcher --nostream    # 输出当前 buffer 后退出(非实时)
pm2 logs polymarket-watcher --raw         # 带 ISO 时间戳(默认是日期格式)

# 直接看日志文件
tail -f logs/pm2-bot.out.log
tail -f logs/pm2-watcher.out.log
ls -lh logs/                              # 所有日志大小

4.4 手动触发测试推送

最常用——停 bot → 追加假信号 → 看 watcher 反应:

cd /opt/PolymarketBTC15mAssistant

pm2 stop polymarket-bot
sleep 2

echo '2026-07-21T18:00:00Z,12.000,3.000,TREND_UP,BUY UP,0.7,0.3,0.5,0.5,0.2,-0.2,ENTER:UP:MID:GOOD' >> logs/signals.csv

sleep 3
pm2 logs polymarket-watcher --lines 30 --nostream | grep -E "slug|sent"

pm2 start polymarket-bot

⚠️ 必须先停 bot:bot 每 1.5s 写一行,会把你的假信号"覆盖"成倒数第二行,watcher 永远读不到。


五、更新代码

cd /opt/PolymarketBTC15mAssistant
git pull
npm install                # 如果 package.json 有变动
pm2 restart all

新代码生效,所有进程短暂重启(约 1-2 秒)。


六、踩过的坑(按遇到顺序)

坑 1token 泄露

症状:用本地硬编码的 token 部署后,token 在对话/聊天记录/屏幕共享中出现 → 任何人可以劫持 bot。

修复

  • 部署前必须 /revoke 拿新 token
  • 服务器 .env 权限 600
  • ecosystem.config.cjsstart.sh.gitignore 里(服务器独有,硬编码路径)
  • 本地 watcher 代码只能读 env var,不能写 fallback

教训:本项目所有密钥类配置走环境变量,文件里绝不出现明文。

坑 2bot JSON dump 永远不触发

症状watcher 日志显示 slug for push: null,市场链接始终没有。ls logs/polymarket_market_*.json 返回空。

根因src/index.js:622 的 dump 条件有 bug

if (poly.ok && poly.market && priceToBeatState.value === null) {
  // dump
}

但 dump 检查之前src/index.js:589-595),priceToBeatState.value 已经被 chainlink 价格锁存成非 null 了。所以这个条件永远 false。

修复commit 4505c11):

if (poly.ok && poly.market) {
  // dump
}

dumpedMarkets.has(slug) 已经保证每个 slug 只 dump 一次,不需要额外的 latch 守卫。

教训:上游项目有 bug,发现后顺手修了,别绕开。

坑 3:双推送(race condition

症状Telegram 同一信号收到 2 次推送,间隔 ~20ms。

根因Node 的 fs.watch 回调 + setTimeout(async ...)

  1. fs.watch 事件 A 触发 → setTimeout 300ms
  2. fs.watch 事件 B 触发(同一个文件事件 inotify 有时会 fire 多次)→ 又一个 setTimeout 300ms
  3. A 的 setTimeout 先到期 → 读文件 → 看到 prevSide=null → flipped=true → await send()
  4. await 让出事件循环
  5. B 的 setTimeout 到期 → 读文件 → 看到 prevSide 还是 null(因为 A 还在 await)→ 又一次 await send()
  6. 两个 send 都完成 → 两条消息

修复commit ee2ab5d):用行时间戳做主去重依据,cooldown 作为兜底:

const ts = data.row[data.hdr.indexOf("timestamp")];
const flipped = side !== prevSide;
const cooled = Date.now() - lastSentAt > COOLDOWN_MS;
const newRow = ts !== lastSentTs;
if (flipped && cooled && newRow) {
  await send(msg);
  lastSentAt = Date.now();
  lastSentTs = ts;
}

每行 CSV 有唯一 ISO 时间戳,同一行永远不会被发两次。

教训:在 Node 异步代码里,单纯用"状态变量做去重"不可靠。用不可变唯一标识(时间戳、行号、UUID)做去重才是正确姿势。

坑 4bot 写入覆盖测试信号

症状:手动 echo 一行假 ENTER 信号到 signals.csvwatcher 没反应,日志没 sent

根因bot 每 1.5s 写一行。在 300ms debounce 窗口里,bot 可能已经写了新行,把测试信号"挤"到倒数第二行。watcher 读最后一行,只看到 bot 的 NO_TRADE。

修复:测试时先 pm2 stop polymarket-bot,让假信号独占最后一行。

教训:往 append-only 日志塞测试数据时,必须停生产者,否则你的数据会被秒盖。

坑 5sed -i 触发不了 fs.watch(误判)

症状:用 sed -i 改 CSV 末尾字段,watcher 没反应。一度怀疑 fs.watch 没在监听。

真相:fs.watch 其实是正常的——后来追加测试时发现它确实在 fire。sed -i 是原子改名(rename),可能某些情况下 inotify 行为不一致,但这个项目里实际工作正常。"没反应"其实是坑 4 的 bot 覆盖问题,误诊了。

教训:先排除更简单的原因(数据被覆盖),再去查底层(fs.watch)。

坑 6:本地 bash 工具跑云端命令

症状bash 工具运行的是本地 Windows,不是 SSH 会话。每次我直接跑 cd /opt/... 都报路径找不到。

修复:只能给用户发命令文本,让用户在 SSH 会话里执行。我本地跑出来的错误输出毫无意义。

教训:远程操作类任务,必须明确"这需要你在服务器执行"。

坑 7watcher 误读 CSV 格式,吞掉全部真信号

症状bot 跑 8 小时,pm2 logs 完全正常,但 Telegram 0 条推送。手动测试用 ENTER:UP:MID:GOOD 格式能推送成功,造成"系统在工作"的假象。

根因watcher 检查 recommendation.startsWith("ENTER"),但 bot 实际写的是 side:phase:strengthsrc/index.js:721,无 ENTER: 前缀):

rec.action === "ENTER" ? `${rec.side}:${rec.phase}:${rec.strength}` : "NO_TRADE"
// 实际输出: "UP:MID:STRONG",不是 "ENTER:UP:MID:STRONG"

→ 7,460 条真信号全被 watcher 判定为 NO_TRADE,只有手动测试用的 ENTER: 前缀格式才会触发。

发现方式:用 analyze.sh 统计 signals.csv 的 recommendation 列分布:

16271 NO_TRADE
 2386 UP:EARLY:STRONG       ← 应该有推送但没推
 1539 DOWN:EARLY:STRONG     ← 同上
 ...

如果信号明显多于推送 → 100% 是格式解析问题。

修复commit 08560d0):用 rec.includes(":") 作为 ENTER 标记:

const isEnter = data.rec.includes(":");
const side = isEnter ? data.rec.split(":")[0] : null;
const [, phase, strength] = data.rec.split(":");

教训

  1. 跨进程/跨文件的数据格式契约必须对照实际生产者代码确认,不能从命名/注释推断。
  2. 设计手动测试时,必须用真实生产数据格式,不能凭直觉编一个"看起来合理"的格式。
  3. "测试能跑" ≠ "系统在工作"——这次手动测试能推送,反而掩盖了真信号被吞的事实。

七、文件结构(部署相关)

七、文件结构(部署相关)

/opt/PolymarketBTC15mAssistant/
├── .env                      # 密钥,gitignorechmod 600
├── .env.example              # 模板,可提交
├── start.sh                  # env wrappergitignore
├── ecosystem.config.cjs      # PM2 配置,gitignore
├── analyze.sh                # 诊断脚本(无推送时用)
├── logs/
│   ├── signals.csv           # bot 写的信号日志
│   ├── polymarket_market_*.json   # 每个市场 dump 一次
│   ├── pm2-bot.out.log
│   ├── pm2-bot.err.log
│   ├── pm2-watcher.out.log
│   └── pm2-watcher.err.log
├── scripts/
│   └── telegram-watcher.js   # 推送 watcher
└── src/
    └── index.js              # 主程序(含 JSON dump 修复)

八、日常运维 checklist

每日

  • pm2 status 看两个进程 online
  • 手机偶尔收条推送确认链路通

每周

  • pm2 logs --lines 200 | grep -i error 看错误
  • du -sh logs/ 看日志增长(signals.csv 会持续增大,可定期归档)

每月

  • npm outdated 看依赖更新
  • 检查 TELEGRAM_BOT_TOKEN 是否需要 rotate
  • df -h /opt 看磁盘空间

异常时

  • pm2 logs <进程> --err --lines 50
  • pm2 restart <进程> 重启单个
  • pm2 restart all 全重启

九、安全 checklist

  • .env 权限 600,仅 root/部署用户可读
  • Telegram bot token 用 /revoke 换过,不是对话里出现过的那个
  • .gitignore 包含 .env.env.*(除 .env.example)、ecosystem.config.cjsstart.sh
  • 没有把含 token 的截图/日志发到聊天软件
  • 服务器 SSH 只允许密钥登录(关密码登录)
  • PM2 进程用普通用户跑(不用 root,除非必要)
  • 修改任何 CSV/JSON 解析逻辑前,先 head -2 logs/signals.csv 看真实字段格式(防坑 7

十、参考命令汇总

# === 部署 ===
cd /opt/PolymarketBTC15mAssistant
npm install
pm2 start ecosystem.config.cjs
pm2 startup && pm2 save

# === 运维 ===
pm2 status
pm2 logs polymarket-bot --lines 50
pm2 restart polymarket-watcher
pm2 stop polymarket-bot && pm2 start polymarket-bot

# === 测试推送 ===
# 注意:格式必须是 side:phase:strength(无 ENTER: 前缀,bot 实际输出格式)
pm2 stop polymarket-bot && sleep 2 && \
echo '2026-07-21T18:00:00Z,12.000,3.000,TREND_UP,BUY UP,0.7,0.3,0.5,0.5,0.2,-0.2,UP:MID:GOOD' >> logs/signals.csv && \
sleep 3 && \
pm2 logs polymarket-watcher --nostream --lines 30 | grep -E "slug|sent" && \
pm2 start polymarket-bot

# === 信号诊断(无推送时先跑这个) ===
./analyze.sh
# 输出 recommendation 分布、max edges、各 phase 的 ENTER 频率
# 真没信号 vs 被吞掉,看 NO_TRADE vs ENTER 类别的比例
# 如果 ENTER 类别 > 0 但没推送 → 解析/推送链路问题

# === 更新 ===
git pull && npm install && pm2 restart all

# === 调试 fs.watch ===
sed -i 's#fs.watch(CSV_PATH, { persistent: true }, () => {#fs.watch(CSV_PATH, { persistent: true }, () => {\n    console.log("[watcher] fs.watch fired", new Date().toISOString());#' scripts/telegram-watcher.js
pm2 restart polymarket-watcher
sleep 10
pm2 logs polymarket-watcher --lines 20 | grep "fs.watch"

十一、关联文件

  • DASHBOARD_GUIDE.md — 仪表盘字段逐项解读(本地阅读用)
  • README.md — 项目官方说明
  • .env.example — 环境变量模板