- 百炼 Token Plan:探针(cookie 网关)每 5 分钟写快照,卡片展示 7 天窗口、订阅到期与重置卡 - 重置卡使用:reset-card-server 本机服务经 Admin Key+卡归属校验后代发 use,前端二次确认;入口默认 CSS 隐藏(localStorage bailian-reset-card=1 开启),探针本体只读不碰 use - 时间周期:新增近 7 天/近 30 天预设,筛选行独立成一行,分组页统计随区间变化 - list/groups 每 5 分钟自动刷新,后台标签页暂停、回窗补拉 - deploy.sh 一键 web/cookie/probe/nginx/status/all;脱敏 env.example,补文档
308 lines
10 KiB
Bash
308 lines
10 KiB
Bash
#!/usr/bin/env bash
|
||
# 一键部署/运维脚本(在本机 Git Bash 里跑,依赖 pnpm、ssh、scp,已配置免密登录)。
|
||
#
|
||
# 用法:
|
||
# bash deploy.sh # = web:构建并发布前端
|
||
# bash deploy.sh web # pnpm build → 上传 dist → 切换站点目录(保留 data/)→ 同步 .env
|
||
# bash deploy.sh cookie # 交互式粘贴新的百炼 Cookie,写入服务器并立即试跑探针
|
||
# bash deploy.sh cookie 'cna=…' # 直接以参数提供 Cookie(会出现在 shell 历史,优先用交互方式)
|
||
# bash deploy.sh probe # 更新探针脚本与 systemd 单元,立即试跑
|
||
# bash deploy.sh nginx # 安装/刷新 nginx 站点配置并 reload
|
||
# bash deploy.sh status # 查看定时器、最新快照与 HTTP 自检
|
||
# bash deploy.sh all # nginx + web + probe(不动 Cookie)
|
||
#
|
||
# 可用环境变量覆盖默认值:
|
||
# DEPLOY_HOST=root@10.1.192.225 REMOTE_DIR=/usr/dcits/token-list-share SITE_PORT=18081
|
||
|
||
set -euo pipefail
|
||
|
||
REMOTE_HOST="${DEPLOY_HOST:-root@10.1.192.225}"
|
||
REMOTE_DIR="${REMOTE_DIR:-/usr/dcits/token-list-share}"
|
||
SITE_PORT="${SITE_PORT:-18081}"
|
||
SUB2API_UPSTREAM="${SUB2API_UPSTREAM:-http://127.0.0.1:18080}"
|
||
|
||
PROBE_ENV=/etc/bailian-probe.env
|
||
PROBE_DIR="$REMOTE_DIR/probe"
|
||
PROBE_BIN="$PROBE_DIR/bailian-probe.py"
|
||
RESET_CARD_BIN="$PROBE_DIR/reset-card-server.py"
|
||
RESET_CARD_PORT="${RESET_CARD_PORT:-18082}"
|
||
SNAPSHOT="$REMOTE_DIR/www/data/bailian.json"
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
log() { printf '\033[1;36m▶ %s\033[0m\n' "$*"; }
|
||
ok() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
|
||
die() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
|
||
|
||
ssh_r() { ssh -o BatchMode=yes "$REMOTE_HOST" "$@"; }
|
||
scp_r() { scp -o BatchMode=yes "$@"; }
|
||
|
||
# ---------- web ----------
|
||
|
||
cmd_web() {
|
||
command -v pnpm >/dev/null || die "找不到 pnpm"
|
||
log "本地构建"
|
||
(cd "$SCRIPT_DIR" && pnpm build)
|
||
|
||
local tmp_tgz; tmp_tgz="$(mktemp -t token-dist-XXXXXX.tar.gz)"
|
||
tar -czf "$tmp_tgz" -C "$SCRIPT_DIR/dist" .
|
||
|
||
log "上传并发布到 $REMOTE_HOST:$REMOTE_DIR"
|
||
scp_r -q "$tmp_tgz" "$REMOTE_HOST:/tmp/token-list-dist.tar.gz"
|
||
# .env 仅作服务器侧备份(key 已构建进 JS),不入 Web 根目录
|
||
scp_r -q "$SCRIPT_DIR/.env" "$REMOTE_HOST:$REMOTE_DIR/.env.new"
|
||
ssh_r 'bash -s' \
|
||
"$REMOTE_DIR" "$SNAPSHOT" <<'REMOTE'
|
||
set -euo pipefail
|
||
DIR="$1"
|
||
mkdir -p "$DIR"
|
||
chmod 600 "$DIR/.env.new"
|
||
mv "$DIR/.env.new" "$DIR/.env"
|
||
mkdir -p "$DIR/www.new"
|
||
tar -xzf /tmp/token-list-dist.tar.gz -C "$DIR/www.new"
|
||
rm -f /tmp/token-list-dist.tar.gz
|
||
# 探针快照目录在站点根内,发版必须保留
|
||
if [ -d "$DIR/www/data" ]; then
|
||
mkdir -p "$DIR/www.new/data"
|
||
cp -a "$DIR/www/data/." "$DIR/www.new/data/"
|
||
fi
|
||
rm -rf "$DIR/www.old"
|
||
mv "$DIR/www" "$DIR/www.old"
|
||
mv "$DIR/www.new" "$DIR/www"
|
||
rm -rf "$DIR/www.old"
|
||
REMOTE
|
||
rm -f "$tmp_tgz"
|
||
ok "前端已发布(data/ 快照目录已保留)"
|
||
}
|
||
|
||
# ---------- cookie ----------
|
||
|
||
read_cookie() {
|
||
# 显式传参(含空字符串,用于清除)优先;其次 BAILIAN_COOKIE 环境变量;最后交互输入
|
||
if [ "$#" -ge 2 ]; then
|
||
printf '%s' "$2"
|
||
return
|
||
fi
|
||
if [ -n "${BAILIAN_COOKIE:-}" ]; then
|
||
printf '%s' "$BAILIAN_COOKIE"
|
||
return
|
||
fi
|
||
printf '粘贴百炼控制台 Cookie(一整行,粘贴后回车;输入留空则清除):\n' >&2
|
||
local line
|
||
IFS= read -r line </dev/tty || true
|
||
printf '%s' "$line"
|
||
}
|
||
|
||
cmd_cookie() {
|
||
local cookie; cookie="$(read_cookie "$@")"
|
||
|
||
# Cookie 经临时文件透传,绝不出现在远程命令行/进程列表里
|
||
local tmp; tmp="$(mktemp -t bailian-cookie-XXXXXX)"
|
||
chmod 600 "$tmp"
|
||
printf '%s' "$cookie" >"$tmp"
|
||
scp_r -q "$tmp" "$REMOTE_HOST:/run/.bailian-cookie.in"
|
||
rm -f "$tmp"
|
||
|
||
log "更新 $PROBE_ENV 并试跑探针"
|
||
ssh_r 'bash -s' "$PROBE_ENV" "$SNAPSHOT" <<'REMOTE'
|
||
set -euo pipefail
|
||
ENV_FILE="$1"; SNAPSHOT="$2"
|
||
ENV_FILE="$ENV_FILE" python3 - <<'PY'
|
||
import os, pathlib
|
||
p = pathlib.Path(os.environ["ENV_FILE"])
|
||
p.parent.mkdir(parents=True, exist_ok=True)
|
||
cookie = pathlib.Path("/run/.bailian-cookie.in").read_text(encoding="utf-8").strip()
|
||
lines = p.read_text().splitlines() if p.exists() else []
|
||
out, found = [], False
|
||
for ln in lines:
|
||
if ln.startswith("BAILIAN_COOKIE="):
|
||
out.append("BAILIAN_COOKIE=" + cookie); found = True
|
||
else:
|
||
out.append(ln)
|
||
if not found:
|
||
out.append("BAILIAN_COOKIE=" + cookie)
|
||
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
||
PY
|
||
chmod 600 "$ENV_FILE"
|
||
rm -f /run/.bailian-cookie.in
|
||
systemctl reset-failed bailian-probe.service >/dev/null 2>&1 || true
|
||
# 凭证失效时 oneshot 退出码为 1,快照仍会写出,属预期
|
||
systemctl start bailian-probe.service >/dev/null 2>&1 || true
|
||
SNAPSHOT="$SNAPSHOT" python3 - <<'PY'
|
||
import json, os, pathlib
|
||
snap = pathlib.Path(os.environ["SNAPSHOT"])
|
||
s = json.loads(snap.read_text(encoding="utf-8"))
|
||
if s.get("ok"):
|
||
print("✓ 探针成功:%s 个用量窗口,probed_at=%s" % (len(s.get("windows", [])), s.get("probed_at")))
|
||
else:
|
||
print("✗ 探针未取到数据(%s):%s" % (s.get("error_kind"), s.get("last_error")))
|
||
PY
|
||
REMOTE
|
||
}
|
||
|
||
# ---------- probe ----------
|
||
|
||
cmd_probe() {
|
||
log "上传探针脚本与 systemd 单元"
|
||
scp_r -q "$SCRIPT_DIR/deploy/bailian-probe.py" "$REMOTE_HOST:/tmp/bailian-probe.py"
|
||
scp_r -q "$SCRIPT_DIR/deploy/reset-card-server.py" "$REMOTE_HOST:/tmp/reset-card-server.py"
|
||
scp_r -q "$SCRIPT_DIR/deploy/bailian-probe.timer" "$REMOTE_HOST:/tmp/bailian-probe.timer"
|
||
ssh_r 'bash -s' "$PROBE_ENV" "$SNAPSHOT" "$PROBE_DIR" "$RESET_CARD_PORT" "$REMOTE_DIR" <<'REMOTE'
|
||
set -euo pipefail
|
||
ENV_FILE="$1"; SNAPSHOT="$2"; PROBE_DIR="$3"; CARD_PORT="$4"; APP_DIR="$5"
|
||
mkdir -p "$PROBE_DIR" "$(dirname "$SNAPSHOT")"
|
||
install -m 755 /tmp/bailian-probe.py "$PROBE_DIR/bailian-probe.py"
|
||
install -m 755 /tmp/reset-card-server.py "$PROBE_DIR/reset-card-server.py"
|
||
install -m 644 /tmp/bailian-probe.timer /etc/systemd/system/bailian-probe.timer
|
||
# 用卡助手:常驻本地服务(只绑 127.0.0.1),用卡前后由 nginx /bailian-admin/ 鉴权转发
|
||
cat > /etc/systemd/system/reset-card-server.service <<EOF
|
||
[Unit]
|
||
Description=百炼重置卡使用助手(本地服务,只读用卡,含 Admin Key 校验)
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
EnvironmentFile=$ENV_FILE
|
||
Environment=BAILIAN_PROBE_PATH=$PROBE_DIR/bailian-probe.py
|
||
Environment=APP_ENV_PATH=$APP_DIR/.env
|
||
Environment=RESET_CARD_BIND=127.0.0.1
|
||
Environment=RESET_CARD_PORT=$CARD_PORT
|
||
ExecStart=/usr/bin/python3 $PROBE_DIR/reset-card-server.py
|
||
Restart=on-failure
|
||
RestartSec=3
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
# service 的 ExecStart 按实际安装路径渲染(仓库模板里写的是 /opt 路径)
|
||
cat > /etc/systemd/system/bailian-probe.service <<EOF
|
||
[Unit]
|
||
Description=阿里云百炼 Token Plan 额度探针
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
EnvironmentFile=$ENV_FILE
|
||
ExecStart=/usr/bin/python3 $PROBE_DIR/bailian-probe.py
|
||
EOF
|
||
rm -f /tmp/bailian-probe.py /tmp/bailian-probe.timer
|
||
# 旧版装在 /opt,迁移后清掉
|
||
rm -rf /opt/bailian-probe
|
||
if [ ! -f "$ENV_FILE" ]; then
|
||
cat > "$ENV_FILE" <<EOF
|
||
BAILIAN_MODE=cookie
|
||
BAILIAN_VARIANT=cn-personal
|
||
BAILIAN_COOKIE=
|
||
BAILIAN_API_KEY=
|
||
BAILIAN_BASE_URL=https://token-plan.cn-beijing.maas.aliyuncs.com
|
||
BAILIAN_OUT=$SNAPSHOT
|
||
EOF
|
||
chmod 600 "$ENV_FILE"
|
||
fi
|
||
systemctl daemon-reload
|
||
systemctl enable --now bailian-probe.timer >/dev/null
|
||
systemctl enable --now reset-card-server.service >/dev/null 2>&1 || true
|
||
systemctl reset-failed bailian-probe.service >/dev/null 2>&1 || true
|
||
systemctl start bailian-probe.service >/dev/null 2>&1 || true
|
||
SNAPSHOT="$SNAPSHOT" python3 - <<'PY'
|
||
import json, os, pathlib
|
||
s = json.loads(pathlib.Path(os.environ["SNAPSHOT"]).read_text(encoding="utf-8"))
|
||
print("快照:ok=%s stale=%s windows=%d err=%s" %
|
||
(s.get("ok"), s.get("stale"), len(s.get("windows", [])), s.get("last_error")))
|
||
PY
|
||
REMOTE
|
||
ok "探针已更新,定时器每 5 分钟跑一次"
|
||
}
|
||
|
||
# ---------- nginx ----------
|
||
|
||
cmd_nginx() {
|
||
log "安装 nginx 站点(端口 $SITE_PORT,反代 $SUB2API_UPSTREAM)"
|
||
local tmp; tmp="$(mktemp -t token-nginx-XXXXXX.conf)"
|
||
cat >"$tmp" <<EOF
|
||
server {
|
||
listen $SITE_PORT;
|
||
listen [::]:$SITE_PORT;
|
||
server_name _;
|
||
|
||
location /api/ {
|
||
proxy_pass $SUB2API_UPSTREAM;
|
||
proxy_http_version 1.1;
|
||
proxy_connect_timeout 300s;
|
||
proxy_send_timeout 300s;
|
||
proxy_read_timeout 300s;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
}
|
||
|
||
# 百炼重置卡用卡助手:只转本地 reset-card-server(其内部再校验 x-api-key)
|
||
location /bailian-admin/ {
|
||
proxy_pass http://127.0.0.1:$RESET_CARD_PORT/;
|
||
proxy_http_version 1.1;
|
||
proxy_connect_timeout 30s;
|
||
proxy_send_timeout 60s;
|
||
proxy_read_timeout 90s;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
}
|
||
|
||
location / {
|
||
root $REMOTE_DIR/www;
|
||
try_files \$uri \$uri/ /index.html;
|
||
}
|
||
}
|
||
EOF
|
||
scp_r -q "$tmp" "$REMOTE_HOST:/etc/nginx/sites-available/token-list"
|
||
ssh_r 'bash -s' <<'REMOTE'
|
||
set -euo pipefail
|
||
ln -sf /etc/nginx/sites-available/token-list /etc/nginx/sites-enabled/token-list
|
||
nginx -t
|
||
systemctl reload nginx
|
||
REMOTE
|
||
ok "nginx 配置已生效"
|
||
}
|
||
|
||
# ---------- status ----------
|
||
|
||
cmd_status() {
|
||
ssh_r 'bash -s' "$SITE_PORT" "$SNAPSHOT" <<'REMOTE'
|
||
set -euo pipefail
|
||
PORT="$1"; SNAPSHOT="$2"
|
||
echo "== timer =="
|
||
systemctl list-timers bailian-probe.timer --no-pager | head -3
|
||
echo "== snapshot =="
|
||
if [ -f "$SNAPSHOT" ]; then
|
||
python3 -c '
|
||
import json, sys
|
||
s = json.load(open(sys.argv[1], encoding="utf-8"))
|
||
print("ok=%s stale=%s windows=%d probed_at=%s" % (s.get("ok"), s.get("stale"), len(s.get("windows", [])), s.get("probed_at")))
|
||
if s.get("last_error"): print("last_error:", s.get("last_error"))
|
||
' "$SNAPSHOT"
|
||
else
|
||
echo "(快照不存在)"
|
||
fi
|
||
echo "== http =="
|
||
for path in / /list.html /groups.html /data/bailian.json /api/v1/admin/groups/all; do
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT$path")
|
||
printf ' %s %s\n' "$code" "$path"
|
||
done
|
||
REMOTE
|
||
}
|
||
|
||
# ---------- main ----------
|
||
|
||
case "${1:-web}" in
|
||
web) cmd_web ;;
|
||
cookie) cmd_cookie "$@" ;;
|
||
probe) cmd_probe ;;
|
||
nginx) cmd_nginx ;;
|
||
status) cmd_status ;;
|
||
all) cmd_nginx; cmd_web; cmd_probe ;;
|
||
*) die "未知子命令:$1(支持 web | cookie | probe | nginx | status | all)" ;;
|
||
esac
|