Files

497 lines
16 KiB
Markdown
Raw Permalink Normal View 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](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` — 环境变量模板