3c3899a115
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
497 lines
16 KiB
Markdown
497 lines
16 KiB
Markdown
# 云端部署指南
|
||
|
||
本文基于实际部署流程编写,记录从代码拉取到稳定运行的完整步骤,以及过程中踩过的所有坑。
|
||
|
||
---
|
||
|
||
## 一、架构概览
|
||
|
||
云端部署两个 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 会话里执行。我本地跑出来的错误输出毫无意义。
|
||
|
||
**教训**:远程操作类任务,必须明确"这需要你在服务器执行"。
|
||
|
||
### 坑 7:watcher 误读 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 # 密钥,gitignore,chmod 600
|
||
├── .env.example # 模板,可提交
|
||
├── start.sh # env wrapper,gitignore
|
||
├── 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` — 环境变量模板 |