Files
token-list-share/deploy.sh

387 lines
14 KiB
Bash
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.
#!/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 auth # 交互式设置 Basic Auth 用户名/密码(不回显)并开启整站认证
# bash deploy.sh auth user pwd # 非交互设置(密码会进 shell 历史,优先用交互方式)
# bash deploy.sh auth off # 关闭 Basic Auth删除 htpasswd 并 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"
HTPASSWD=/etc/nginx/token-list.htpasswd
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 分钟跑一次"
}
# ---------- auth ----------
# 在远端用 openssl apr1 生成 htpasswd 行Debian/Ubumtu 的 openssl 带 -apr1
cmd_auth() {
if [ "${2:-}" = "off" ]; then
log "关闭 Basic Auth"
ssh_r "rm -f '$HTPASSWD'"
cmd_nginx
ok "Basic Auth 已关闭"
return
fi
local user password
if [ "$#" -ge 3 ]; then
user="$2"; password="$3"
else
printf 'Basic Auth 用户名:' >&2
IFS= read -r user </dev/tty || true
printf '密码(输入不回显):' >&2
IFS= read -r -s password </dev/tty || true
printf '\n' >&2
fi
[ -n "$user" ] || die "用户名不能为空"
[ -n "$password" ] || die "密码不能为空"
# 密码经 600 临时文件透传,不出现在远端命令行/进程列表
local tmp; tmp="$(mktemp -t token-htpasswd-XXXXXX)"
chmod 600 "$tmp"
printf '%s' "$password" >"$tmp"
scp_r -q "$tmp" "$REMOTE_HOST:/run/.token-htpasswd.in"
rm -f "$tmp"
log "写入 $HTPASSWD 并刷新 nginx"
# stdout 输出除本次用户外仍保留的账号,用于提示旧账号仍可登录
local others
others="$(ssh_r 'bash -s' "$HTPASSWD" "$user" <<'REMOTE'
set -euo pipefail
HTPASSWD="$1"; USER_NAME="$2"
command -v openssl >/dev/null || { echo "✗ 远端没有 openssl无法生成 apr1 哈希" >&2; exit 1; }
[ -s /run/.token-htpasswd.in ] || { echo "✗ 密码临时文件缺失" >&2; exit 1; }
hash=$(openssl passwd -apr1 -in /run/.token-htpasswd.in)
rm -f /run/.token-htpasswd.in
mkdir -p "$(dirname "$HTPASSWD")"
# 同名用户覆盖更新,其余用户保留
if [ -f "$HTPASSWD" ]; then
grep -v "^${USER_NAME}:" "$HTPASSWD" > "$HTPASSWD.new" || true
else
: > "$HTPASSWD.new"
fi
printf '%s:%s\n' "$USER_NAME" "$hash" >> "$HTPASSWD.new"
install -m 640 "$HTPASSWD.new" "$HTPASSWD"
rm -f "$HTPASSWD.new"
chown root:www-data "$HTPASSWD" 2>/dev/null || chown root:nginx "$HTPASSWD" 2>/dev/null || true
cut -d: -f1 "$HTPASSWD" | grep -v "^${USER_NAME}$" || true
REMOTE
)"
cmd_nginx >/dev/null
ok "Basic Auth 已开启:用户 $user(浏览器访问整站时输入)"
if [ -n "$others" ]; then
printf '\033[1;33m! htpasswd 里还有其他账号仍可登录:%s\n' "$(printf '%s' "$others" | tr '\n' ' ')" >&2
printf ' 如要停用旧账号ssh %s "sed -i \x27/^<用户名>:/d\x27 %s"\033[0m\n' "$REMOTE_HOST" "$HTPASSWD" >&2
fi
}
# ---------- nginx ----------
cmd_nginx() {
log "安装 nginx 站点(端口 $SITE_PORT,反代 $SUB2API_UPSTREAM"
# 先看远端 htpasswd 是否存在:存在就在整站 server 块开 Basic Auth
local auth_block=""
if ssh_r "test -s '$HTPASSWD'"; then
auth_block="$(printf ' auth_basic "token-list-share";\n auth_basic_user_file %s;\n' "$HTPASSWD")"
log "检测到 $HTPASSWD,开启 Basic Auth"
fi
local tmp; tmp="$(mktemp -t token-nginx-XXXXXX.conf)"
cat >"$tmp" <<EOF
server {
listen $SITE_PORT;
listen [::]:$SITE_PORT;
server_name _;
$auth_block
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" "$HTPASSWD" <<'REMOTE'
set -euo pipefail
PORT="$1"; SNAPSHOT="$2"; HTPASSWD="$3"
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 =="
# 若开了 Basic Auth无凭证应统一 401哈希无法回放密码带凭证 200 需手工 curl -u 验证)
if [ -s "$HTPASSWD" ]; then
echo "Basic Auth 已开启:下面无凭证请求预期均为 401"
fi
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 ;;
auth) cmd_auth "$@" ;;
status) cmd_status ;;
all) cmd_nginx; cmd_web; cmd_probe ;;
*) die "未知子命令:$1(支持 web | cookie | probe | nginx | auth | status | all" ;;
esac