From 5cafbfa775c36702934aa33ccb7e61836352b67e Mon Sep 17 00:00:00 2001 From: gavindiaz Date: Tue, 21 Jul 2026 18:42:52 +0800 Subject: [PATCH] Add DEPLOYMENT.md: full cloud deployment guide 11 sections covering architecture, prerequisites, one-time deploy, day-to-day ops, code updates, 6 real-world pitfalls with root cause + fix commits, file layout, checklists, and cheat sheet. --- DEPLOYMENT.md | 450 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 DEPLOYMENT.md diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..a6975c3 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,450 @@ +# 云端部署指南 + +本文基于实际部署流程编写,记录从代码拉取到稳定运行的完整步骤,以及过程中踩过的所有坑。 + +--- + +## 一、架构概览 + +云端部署两个 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](https://t.me/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 服务器要求 + +- Linux(Ubuntu 22.04+ 测试通过) +- Node.js 18+(推荐 20.x) +- systemd(用于 PM2 开机自启) +- 内存 ≥ 512MB +- 国内云服务器需要代理访问 Telegram/Polymarket + +--- + +## 三、一次性部署流程 + +### 3.1 服务器基础环境 + +```bash +# 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 拉取代码 + +```bash +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 进程。 + +```bash +cp .env.example .env +nano .env +``` + +填入实际值: + +```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 # 国内云服务器需要 +``` + +收紧权限: + +```bash +chmod 600 .env +``` + +### 3.4 创建 `start.sh`(env wrapper) + +这个文件**不在 git 里**(gitignore 了),服务器要现场写: + +```bash +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.cjs`(PM2 配置) + +同样 gitignore,路径改成 `/opt/...`: + +```bash +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 启动 + 开机自启 + +```bash +# 启动 +pm2 start ecosystem.config.cjs + +# 开机自启(复制输出的 sudo 命令执行) +pm2 startup +# 例:sudo env PATH=... systemctl enable pm2-root + +# 保存当前进程列表(开机时按此恢复) +pm2 save +``` + +⚠️ `pm2 startup` 和 `pm2 save` **两个都要做**: +- `startup` 生成 systemd unit 让 PM2 守护进程开机启动 +- `save` 保存当前进程列表,让 PM2 启动时知道要 resurrect 哪些 app + +--- + +## 四、常用操作速查 + +### 4.1 状态查看 + +```bash +pm2 status # 所有进程概览 +pm2 monit # 实时 CPU/内存监控(curses 界面) +pm2 list # 同 status 但更紧凑 +pm2 describe polymarket-bot # 单个进程详细信息 +pm2 env polymarket-watcher # 进程实际 env vars +``` + +### 4.2 启停重启 + +```bash +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 日志查看 + +```bash +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 反应: + +```bash +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 永远读不到。 + +--- + +## 五、更新代码 + +```bash +cd /opt/PolymarketBTC15mAssistant +git pull +npm install # 如果 package.json 有变动 +pm2 restart all +``` + +新代码生效,所有进程短暂重启(约 1-2 秒)。 + +--- + +## 六、踩过的坑(按遇到顺序) + +### 坑 1:token 泄露 + +**症状**:用本地硬编码的 token 部署后,token 在对话/聊天记录/屏幕共享中出现 → 任何人可以劫持 bot。 + +**修复**: +- 部署前必须 `/revoke` 拿新 token +- 服务器 `.env` 权限 600 +- `ecosystem.config.cjs` 和 `start.sh` 在 `.gitignore` 里(服务器独有,硬编码路径) +- 本地 watcher 代码**只能读 env var**,不能写 fallback + +**教训**:本项目所有密钥类配置走环境变量,文件里绝不出现明文。 + +### 坑 2:bot JSON dump 永远不触发 + +**症状**:watcher 日志显示 `slug for push: null`,市场链接始终没有。`ls logs/polymarket_market_*.json` 返回空。 + +**根因**:`src/index.js:622` 的 dump 条件有 bug: +```js +if (poly.ok && poly.market && priceToBeatState.value === null) { + // dump +} +``` +但 dump 检查**之前**(`src/index.js:589-595`),`priceToBeatState.value` 已经被 chainlink 价格锁存成非 null 了。所以这个条件永远 false。 + +**修复**(commit `4505c11`): +```js +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 作为兜底: + +```js +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)做去重**才是正确姿势。 + +### 坑 4:bot 写入覆盖测试信号 + +**症状**:手动 `echo` 一行假 ENTER 信号到 signals.csv,watcher 没反应,日志没 `sent`。 + +**根因**:bot 每 1.5s 写一行。在 300ms debounce 窗口里,bot 可能已经写了新行,把测试信号"挤"到倒数第二行。watcher 读最后一行,只看到 bot 的 NO_TRADE。 + +**修复**:测试时先 `pm2 stop polymarket-bot`,让假信号独占最后一行。 + +**教训**:往 append-only 日志塞测试数据时,**必须停生产者**,否则你的数据会被秒盖。 + +### 坑 5:sed -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 会话里执行。我本地跑出来的错误输出毫无意义。 + +**教训**:远程操作类任务,必须明确"这需要你在服务器执行"。 + +--- + +## 七、文件结构(部署相关) + +``` +/opt/PolymarketBTC15mAssistant/ +├── .env # 密钥,gitignore,chmod 600 +├── .env.example # 模板,可提交 +├── start.sh # env wrapper,gitignore +├── ecosystem.config.cjs # PM2 配置,gitignore +├── 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.cjs`、`start.sh` +- [ ] 没有把含 token 的截图/日志发到聊天软件 +- [ ] 服务器 SSH 只允许密钥登录(关密码登录) +- [ ] PM2 进程用普通用户跑(不用 root,除非必要) + +--- + +## 十、参考命令汇总 + +```bash +# === 部署 === +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 + +# === 测试推送 === +pm2 stop polymarket-bot && sleep 2 && \ +echo '...ENTER:UP:MID:GOOD' >> logs/signals.csv && \ +sleep 3 && \ +pm2 logs polymarket-watcher --nostream --lines 30 | grep -E "slug|sent" && \ +pm2 start polymarket-bot + +# === 更新 === +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` — 环境变量模板 \ No newline at end of file