Files
Chan/live/deploy/install.sh
T
jackandCursor 999ad924ad install.sh 报出模板新增的配置项,并给 --sync-env 追加
live.env 装着密钥,所以安装时刻意不覆盖。但这样一来,往 live.env.example
里加配置,已有部署会永远拿不到、且毫无提示——静默漂移,等到某个开关根本
没生效才发现。这次加 TG_TOKEN 就撞上了。

现在重复安装会 diff 两边的变量名:报模板多出的项(提示跑 --sync-env),
也报配置里已被模板删掉的项(可能已废弃)。

--sync-env 独立成一个模式而不是塞进安装流程:追加要改一个装着密钥的文件,
这种事应当由人显式发起。只追加缺的项、连它上面的注释一起,不动已有任何
一行,先备份。实测原有内容逐行未变且重复跑幂等。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 17:45:48 +08:00

196 lines
9.0 KiB
Bash
Executable File
Raw 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.
#!/usr/bin/env bash
# 在生产机(有 Bitget API key 白名单的那台)上装实盘执行器。
#
# 只装执行侧:标准库 + aiohttp。**不装** Docker / Hummingbot / pandas /
# chanlun 引擎——那些是采集与研究侧的依赖,理由见 live/live_exec.py 文件头。
#
# sudo ./install.sh # 从当前 checkout 安装
# sudo REPO=git@host:jack/chan.git ./install.sh # 或指定远程克隆
#
# 幂等:重复跑只会更新代码与依赖,不动 /etc/chan-live/live.env 和状态目录。
set -euo pipefail
APP=/opt/chan
STATE=/var/lib/chan-live
CONF=/etc/chan-live
USER_NAME=chan
BRANCH="${BRANCH:-chan}"
REPO="${REPO:-}"
die() { echo "⛔ $*" >&2; exit 1; }
say() { echo " $*"; }
[[ $EUID -eq 0 ]] || die "要 rootsudo $0"
# ── --sync-env:把模板新增的项追加到已有配置 ────────────────────────
# 独立成一个模式而不是塞进安装流程:追加会改一个装着密钥的文件,这种事应当
# 由你显式发起。只追加缺的项(连它上面的注释一起),**不动**已有任何一行。
if [[ "${1:-}" == "--sync-env" ]]; then
EX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/live.env.example"
[[ -f "$EX" ]] || die "找不到模板 $EX"
[[ -f "$CONF/live.env" ]] || die "$CONF/live.env 还不存在,先跑一次 sudo $0"
cp -a "$CONF/live.env" "$CONF/live.env.bak.$(date +%Y%m%d%H%M%S)"
python3 - "$EX" "$CONF/live.env" <<'PY'
import re
import sys
ex_path, live_path = sys.argv[1], sys.argv[2]
ex = open(ex_path, encoding="utf-8").read().splitlines()
live = open(live_path, encoding="utf-8").read()
KEY = re.compile(r"^([A-Z_][A-Z0-9_]*)=")
have = set(KEY.match(x).group(1) for x in live.splitlines() if KEY.match(x))
add, block = [], []
for line in ex:
if KEY.match(line):
k = KEY.match(line).group(1)
if k not in have:
add += block + [line]
block = []
elif line.startswith("#") or (not line.strip() and block):
block.append(line)
else:
block = []
if not add:
print(" 配置已是最新,无需追加")
raise SystemExit(0)
with open(live_path, "a", encoding="utf-8") as f:
f.write("\n\n# ── 以下由 install.sh --sync-env 追加 ──\n")
f.write("\n".join(add) + "\n")
n = sum(1 for x in add if KEY.match(x))
print(f" 追加了 {n} 项:" +
" ".join(KEY.match(x).group(1) for x in add if KEY.match(x)))
PY
say "原文件已备份为 $CONF/live.env.bak.*"
say "追加的项多半是空值,逐项填完再重启:sudo systemctl restart chan-live-exec"
exit 0
fi
# ── 1. 系统依赖 ────────────────────────────────────────────────────
say "装系统包"
if command -v apt-get >/dev/null; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
# chrony 不是可选项:staleness 闸靠两机时钟一致才有意义,
# 采集机时钟快 5 分钟就等于把闸放宽 5 分钟(见 ship_signals.py
apt-get install -y -qq python3-venv python3-pip git chrony openssh-client
elif command -v dnf >/dev/null; then
dnf install -y -q python3 python3-pip git chrony openssh-clients
else
die "只认 apt/dnf,其他发行版请手工装 python3-venv git chrony"
fi
systemctl enable --now chrony 2>/dev/null || systemctl enable --now chronyd
# ── 2. 专用用户与目录 ──────────────────────────────────────────────
if ! id -u "$USER_NAME" >/dev/null 2>&1; then
say "建系统用户 $USER_NAME(无登录 shell"
useradd --system --home-dir "$STATE/home" --create-home \
--shell /usr/sbin/nologin "$USER_NAME"
fi
install -d -o "$USER_NAME" -g "$USER_NAME" -m 750 "$STATE" "$STATE/state" "$STATE/home"
install -d -o "$USER_NAME" -g "$USER_NAME" -m 700 "$STATE/home/.ssh"
install -d -o root -g "$USER_NAME" -m 750 "$CONF"
# ── 3. 代码 ────────────────────────────────────────────────────────
if [[ -n "$REPO" ]]; then
if [[ -d "$APP/.git" ]]; then
say "更新已有 checkout"
git -C "$APP" fetch --quiet origin "$BRANCH"
git -C "$APP" checkout --quiet "$BRANCH"
git -C "$APP" reset --hard --quiet "origin/$BRANCH"
else
say "克隆 $REPO"
rm -rf "$APP"; git clone --quiet --branch "$BRANCH" "$REPO" "$APP"
fi
else
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
[[ -f "$SRC/live/live_exec.py" ]] || die "$SRC 不像仓库根(缺 live/live_exec.py"
if [[ "$SRC" != "$APP" ]]; then
say "从 $SRC 同步代码到 $APP"
install -d "$APP"
# 只同步生产要的那一个子树。研究侧的 research/ 不上生产机:
# 它带 pandas/pyarrow/hummingbot,而且探针 OOM 过一次(14.9GB)
cp -a "$SRC/live" "$APP/"
fi
fi
chown -R root:root "$APP/live"
find "$APP/live" -type f -exec chmod 644 {} + ; chmod 755 "$APP/live" "$APP/live/deploy"
chmod 755 "$APP"/live/deploy/*.sh
# ── 4. venv ───────────────────────────────────────────────────────
say "建 venv 并装依赖(应当只有 aiohttp"
[[ -d "$APP/.venv" ]] || python3 -m venv "$APP/.venv"
"$APP/.venv/bin/pip" install --quiet --upgrade pip
"$APP/.venv/bin/pip" install --quiet -r "$APP/live/requirements.txt"
# 断言生产进程没被拖进重量级依赖。这条会真挡住——曾经为读两个常量
# import 研究侧的 step43,把 numpy/pandas/pyarrow 全拉进实盘进程
say "验依赖面"
"$APP/.venv/bin/python" - <<'PY' || die "生产进程拖进了重量级依赖,看上面输出"
import sys
sys.path.insert(0, "/opt/chan/live")
import live_exec
live_exec.assert_decomposable()
heavy = [m for m in ("numpy", "pandas", "pyarrow", "hummingbot", "scipy")
if m in sys.modules]
if heavy:
print(f" ⛔ 启动路径加载了 {heavy}")
raise SystemExit(1)
print(f" ✓ 只有标准库 + aiohttp · 出场结构 "
f"{live_exec.SL_ATR}/{live_exec.SCALE_ATR}/{live_exec.RUNNER_ATR} ATR "
f"/{live_exec.MAXB} 根")
PY
# ── 5. 配置模板 ────────────────────────────────────────────────────
if [[ ! -f "$CONF/live.env" ]]; then
say "写配置模板 $CONF/live.env(密钥要你手工填)"
install -o root -g "$USER_NAME" -m 640 \
"$APP/live/deploy/live.env.example" "$CONF/live.env"
NEED_FILL=1
else
say "$CONF/live.env 已存在,不覆盖(里面是密钥)"
# 但要报出模板新增的项。不报的话,以后往 example 里加配置,已有部署会
# **永远拿不到且毫无提示**——静默漂移,等到出事才发现某个开关根本没生效
keys() { grep -oE '^[A-Z_][A-Z0-9_]*=' "$1" | tr -d '=' | sort -u; }
MISSING="$(comm -23 <(keys "$APP/live/deploy/live.env.example") \
<(keys "$CONF/live.env") | tr '\n' ' ')"
if [[ -n "${MISSING// }" ]]; then
say "⚠ 模板比你的配置多了这些项:$MISSING"
say " 逐项看说明:$APP/live/deploy/live.env.example"
say " 要把缺的那几行连注释一起追加过去,跑:"
say " sudo $APP/live/deploy/install.sh --sync-env"
NEED_FILL=1
fi
OBSOLETE="$(comm -13 <(keys "$APP/live/deploy/live.env.example") \
<(keys "$CONF/live.env") | tr '\n' ' ')"
[[ -n "${OBSOLETE// }" ]] && \
say " 另有模板里已没有的项(可能已废弃):$OBSOLETE"
fi
# ── 6. systemd ────────────────────────────────────────────────────
say "装 systemd 单元"
install -m 644 "$APP"/live/deploy/chan-live-*.service /etc/systemd/system/
systemctl daemon-reload
echo
echo "装好了。接下来按顺序做(**不要**跳过空跑那步):"
echo
if [[ -n "${NEED_FILL:-}" ]]; then
echo " 1. 填密钥与参数:sudo vi $CONF/live.env"
echo " BITGET_API_KEY / SECRET / PASSPHRASE 用只读+交易权限,"
echo " **不要开提币权限**。SHIP_FROM 填采集机的 ssh 目标。"
echo
fi
echo " 2. 装拉总线用的 ssh key,并确认能连上采集机:"
echo " sudo -u $USER_NAME ssh-keygen -t ed25519 -N '' -f $STATE/home/.ssh/id_ed25519"
echo " # 把 $STATE/home/.ssh/id_ed25519.pub 加到采集机的 authorized_keys"
echo " sudo -u $USER_NAME ssh -o BatchMode=yes <采集机> 'echo ok'"
echo
echo " 3. 空跑验全链(不下真单,跑够看到一次心跳再停):"
echo " sudo $APP/live/deploy/dryrun.sh"
echo
echo " 4. 真跑:"
echo " sudo systemctl enable --now chan-live-ship chan-live-exec"
echo " $APP/live/deploy/status.sh"