接入百炼额度探针、重置卡管理与周期/自动刷新升级

- 百炼 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,补文档
This commit is contained in:
2026-09-14 15:40:35 +08:00
parent 17d3bf03bf
commit 2419141284
18 changed files with 1771 additions and 42 deletions

307
deploy.sh Normal file
View File

@@ -0,0 +1,307 @@
#!/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