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

497 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 云端部署指南
本文基于实际部署流程编写,记录从代码拉取到稳定运行的完整步骤,以及过程中踩过的所有坑。
---
## 一、架构概览
云端部署两个 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 服务器要求
- LinuxUbuntu 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 秒)。
---
## 六、踩过的坑(按遇到顺序)
### 坑 1token 泄露
**症状**:用本地硬编码的 token 部署后,token 在对话/聊天记录/屏幕共享中出现 → 任何人可以劫持 bot。
**修复**
- 部署前必须 `/revoke` 拿新 token
- 服务器 `.env` 权限 600
- `ecosystem.config.cjs` 和 `start.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
```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.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:strength``src/index.js:721`,无 `ENTER:` 前缀):
```js
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 标记:
```js
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.cjs`、`start.sh`
- [ ] 没有把含 token 的截图/日志发到聊天软件
- [ ] 服务器 SSH 只允许密钥登录(关密码登录)
- [ ] PM2 进程用普通用户跑(不用 root,除非必要)
- [ ] **修改任何 CSV/JSON 解析逻辑前,先 `head -2 logs/signals.csv` 看真实字段格式**(防坑 7
---
## 十、参考命令汇总
```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
# === 测试推送 ===
# 注意:格式必须是 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` — 环境变量模板