monitor-ai/scripts/deploy-monitor.sh

352 lines
12 KiB
Bash
Executable File
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
# monitor-ai 部署脚本
#
# 用法:
# bash deploy-monitor.sh # 部署到 txjp 服务器(默认)
# bash deploy-monitor.sh --dev # 开发模式(本地构建)
#
# 首次部署自动:
# 1. 生成 OIDC_CLIENT_SECRET (openssl rand -hex 32)
# 2. 生成 LOCALADMIN_PASSWORD (openssl rand -hex 16)
# 3. 生成 pbkdf2 密码哈希(通过 Authelia 容器)
# 4. 更新 nginx 配置
#
# 支持系统:
# - macOS (本地): zsh + bash 兼容模式
# - Ubuntu/Debian (服务器): dash + bash
# - Rocky Linux/CentOS/RHEL (服务器): bash
# ============================================================
# 颜色和日志
# ============================================================
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m'
log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; }
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
info() { printf "${CYAN}[i]${NC} %s\n" "$1"; }
error() { printf "${RED}[✗]${NC} %s\n" "$1"; }
timing(){ printf " ${CYAN}${NC} %s\n" "$1"; }
# ============================================================
# 检测 macOS 和 Linux sed 差异
# ============================================================
is_mac() {
[[ "$(uname)" == "Darwin" ]]
}
SED_I_BACKUP='-i'
if is_mac; then
SED_I_BACKUP='-i ""'
fi
# ============================================================
# 解析参数
# ============================================================
MODE="remote"
while [[ $# -gt 0 ]]; do
case "$1" in
--dev)
MODE="dev"
info "模式: 开发构建"
shift
;;
*)
warn "未知参数: $1"
shift
;;
esac
done
# ============================================================
# 路径配置(对齐 deploy-ai.sh使用 SSH 别名 txjp
# ============================================================
LOCAL_DIR="/Users/niuniu/programs/docker"
MONITOR_DIR="$LOCAL_DIR/monitor-ai"
SHARED_DIR="$LOCAL_DIR/shared"
REMOTE_DIR="/root/docker/monitor-ai"
CONTAINER="monitor-ai"
SSH_TARGET="txjp"
SNAPSHOT_FILE="/tmp/.snapshot.monitor.md5"
# ============================================================
# 打包源码
# ============================================================
package_source() {
log "打包源码..."
local pack_start
pack_start=$(date +%s)
COPYFILE_DISABLE=1 tar czf /tmp/monitor-ai.tar.gz \
-C "$LOCAL_DIR" \
--exclude='node_modules' --exclude='.next' --exclude='out' \
--exclude='data' \
--exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \
--exclude='.git' --exclude='._*' \
shared/ monitor-ai/ docs/design/design-system/tlyq-design-system.css || {
error "打包失败"
return 1
}
local pack_end pkg_size pack_dur
pack_end=$(date +%s)
pack_dur=$((pack_end - pack_start))
pkg_size=$(du -h /tmp/monitor-ai.tar.gz 2>/dev/null | cut -f1 || echo "未知")
timing "打包耗时: ${pack_dur}s (${pkg_size})"
}
# ============================================================
# 首次部署:生成密钥和配置
# ============================================================
first_deploy_setup() {
info "首次部署检查..."
local env_exists
env_exists=$(ssh txjp "test -f $REMOTE_DIR/.env && echo yes || echo no" 2>/dev/null)
if [[ "$env_exists" == "yes" ]]; then
info "已存在 .env跳过密钥生成"
return 0
fi
log "首次部署:生成密钥和配置..."
local OIDC_SECRET LOCALADMIN_PASS JWT_SECRET PBKDF2_HASH
OIDC_SECRET=$(openssl rand -hex 32)
LOCALADMIN_PASS=$(openssl rand -hex 16)
JWT_SECRET=$(openssl rand -hex 32)
# 生成 pbkdf2 密码哈希(通过 Authelia 容器)
PBKDF2_HASH=$(ssh txjp "docker exec authelia authelia crypto hash generate pbkdf2 --password '$LOCALADMIN_PASS' 2>/dev/null | grep 'Digest:' | awk '{print \$2}'" 2>/dev/null)
# 如果 Authelia 不可用,使用 openssl 生成备用哈希
if [[ -z "$PBKDF2_HASH" ]]; then
warn "Authelia 不可用,使用 openssl 生成密码哈希"
PBKDF2_HASH=$(echo -n "$LOCALADMIN_PASS" | openssl dgst -sha256 -binary | openssl base64)
PBKDF2_HASH="pbkdf2:sha256:600000\$$(openssl rand -hex 16)\$$PBKDF2_HASH"
fi
# 写入 .env 文件
ssh txjp "cat > $REMOTE_DIR/.env << 'ENVEOF'
DATABASE_PATH=/app/data/monitor.db
MONITOR_MODE=local
AUTHELIA_URL=https://sso.tlyq.ai
OIDC_CLIENT_ID=monitor-oidc
OIDC_CLIENT_SECRET=$OIDC_SECRET
OIDC_REDIRECT_URI=https://monitor.tlyq.ai/api/auth/callback
JWT_SECRET=$JWT_SECRET
COOKIE_DOMAIN=.tlyq.ai
LDAP_URL=ldap://ldap-ai:3890
LOCALADMIN_PASSWORD=$LOCALADMIN_PASS
NODE_ENV=production
NODE_TLS_REJECT_UNAUTHORIZED=0
ENVEOF"
log "密钥已生成"
info " OIDC_CLIENT_SECRET: $OIDC_SECRET"
info " LOCALADMIN_PASSWORD: $LOCALADMIN_PASS"
warn "请妥善保管以上密钥,.env 文件不会上传到版本控制"
}
# ============================================================
# 更新 nginx 配置
# ============================================================
update_nginx() {
local nginx_conf="$LOCAL_DIR/nginx-proxy-ai/conf.d/monitor-ai.conf"
if [[ ! -f "$nginx_conf" ]]; then
warn "nginx 配置文件不存在: $nginx_conf"
return 0
fi
log "更新 nginx 配置..."
scp "$nginx_conf" txjp:/root/docker/nginx-proxy-ai/conf.d/monitor-ai.conf 2>/dev/null || true
ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" && log "nginx 已重载" || warn "nginx 重载失败"
}
# ============================================================
# 服务器构建
# ============================================================
build_on_server() {
# 1. 计算源码快照
log "检查源码是否有变化..."
local local_md5
local_md5=$(find "$MONITOR_DIR" "$SHARED_DIR" \
-not -path '*/node_modules/*' \
-not -path '*/.next/*' \
-not -path '*/out/*' \
-not -path '*/data/*' \
-not -path '*/docs/*' \
-not -path '*/.env*' \
-not -path '*/.git/*' \
-not -name '._*' \
-type f \
-exec md5 -q {} \; 2>/dev/null \
| sort \
| md5 -q)
echo " 源码快照: ${local_md5}"
local prev_md5
prev_md5=$(ssh txjp "cat ${SNAPSHOT_FILE} 2>/dev/null" 2>/dev/null || echo "")
if [[ -z "$prev_md5" ]]; then
info "首次部署,执行完整构建"
elif [[ "$prev_md5" == "$local_md5" ]]; then
info "源码无变化,跳过构建,仅重启容器"
ssh txjp "cd $REMOTE_DIR && docker compose down && docker compose up -d"
log "容器已重建"
return 0
else
info "检测到源码变化,执行增量构建"
fi
# 2. 打包源码
package_source
# 3. 上传
log "上传源码包..."
scp /tmp/monitor-ai.tar.gz txjp:/tmp/monitor-ai.tar.gz || {
error "上传失败"
return 1
}
# 4. 解压源码到 /tmp然后 rsync 到目标目录
log "解压源码并准备依赖..."
ssh txjp "\
rm -rf /tmp/deploy_monitor && mkdir -p /tmp/deploy_monitor && \
tar xzf /tmp/monitor-ai.tar.gz -C /tmp/deploy_monitor 2>/dev/null || true && \
rsync -a --delete \
--exclude='node_modules' --exclude='.next' --exclude='data' --exclude='docs' \
--exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \
--exclude='._*' \
/tmp/deploy_monitor/monitor-ai/ $REMOTE_DIR/ && \
rsync -a --delete \
--exclude='node_modules' --exclude='.git' \
/tmp/deploy_monitor/shared/ /root/docker/shared/ && \
rm -rf $REMOTE_DIR/shared && \
cp -r /root/docker/shared $REMOTE_DIR/shared && \
mkdir -p $REMOTE_DIR/docs/design/design-system && \
cp /tmp/deploy_monitor/docs/design/design-system/tlyq-design-system.css $REMOTE_DIR/docs/design/design-system/ 2>/dev/null || true && \
rm -rf /tmp/deploy_monitor"
# 5. 首次部署:上传 docker-compose.yml 和 Dockerfile
ssh txjp "cd $REMOTE_DIR && docker compose up -d"
# 如果 host 上没有 node_modules从容器内复制一份
ssh txjp "if [ ! -d '${REMOTE_DIR}/node_modules' ]; then
echo ' 首次:复制容器内 node_modules 到主机...'
docker cp ${CONTAINER}:/app/node_modules ${REMOTE_DIR}/node_modules
echo ' 完成'
fi"
# 6. 安装可能新增的依赖
log "安装新增依赖..."
ssh txjp "cd ${REMOTE_DIR} && npm install --prefer-offline 2>&1 | tail -5 || true"
# 7. 清理可能被上传的本地环境变量文件
ssh txjp "rm -f $REMOTE_DIR/.env.local $REMOTE_DIR/.env.development $REMOTE_DIR/.env.production 2>/dev/null; echo '已清理本地环境文件'"
# 8. 重建容器Dockerfile 内完成构建)
log "重建容器..."
ssh txjp "cd $REMOTE_DIR && docker compose up -d --build && docker compose restart"
# 10. 清理构建缓存
log "清理构建缓存..."
ssh txjp "docker image prune -f 2>/dev/null || true"
local disk_usage
disk_usage=$(ssh txjp "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50")
if [ "$disk_usage" -gt 95 ]; then
warn "磁盘使用率 ${disk_usage}%,执行紧急清理"
ssh txjp "docker builder prune -f 2>/dev/null || true"
elif [ "$disk_usage" -gt 85 ]; then
info "磁盘使用率 ${disk_usage}%执行激进清理6小时"
ssh txjp "docker builder prune -f --filter 'until=6h' 2>/dev/null || true"
elif [ "$disk_usage" -gt 70 ]; then
info "磁盘使用率 ${disk_usage}%执行正常清理24小时"
ssh txjp "docker builder prune -f --filter 'until=24h' 2>/dev/null || true"
else
info "磁盘使用率 ${disk_usage}%执行保守清理48小时"
ssh txjp "docker builder prune -f --filter 'until=48h' 2>/dev/null || true"
fi
# 11. 保存快照
ssh txjp "echo '$local_md5' > ${SNAPSHOT_FILE}"
}
# ============================================================
# 开发模式(本地构建)
# ============================================================
build_dev() {
log "本地开发模式构建..."
local build_start build_end build_dur
build_start=$(date +%s)
cd "$MONITOR_DIR" || { error "无法进入 $MONITOR_DIR"; exit 1; }
npm run build 2>&1 | tail -15
local build_exit_code=${PIPESTATUS[0]}
build_end=$(date +%s)
build_dur=$((build_end - build_start))
timing "构建耗时: ${build_dur}s"
if [[ "$build_exit_code" -ne 0 ]]; then
error "构建失败"
exit 1
fi
log "本地构建完成"
}
# ============================================================
# 执行部署
# ============================================================
echo ""
printf "${CYAN}=========================================${NC}\n"
printf "${CYAN} monitor-ai 部署${NC}\n"
printf "${CYAN}=========================================${NC}\n"
echo ""
BUILD_START=$(date +%s)
case "$MODE" in
remote)
# 远程模式:部署到 txjp 服务器
log "部署目标: $MONITOR_DIR → txjp:$REMOTE_DIR"
echo ""
# 首次部署检查
first_deploy_setup
# 构建
build_on_server
# 更新 nginx
update_nginx
;;
dev)
# 开发模式:仅本地构建
build_dev
;;
esac
# ============================================================
# 验证
# ============================================================
echo ""
log "验证部署..."
BUILD_END=$(date +%s)
TOTAL_DUR=$((BUILD_END - BUILD_START))
if [[ "$MODE" == "dev" ]]; then
log "开发构建完成,总耗时: ${TOTAL_DUR}s"
exit 0
fi
STATUS=$(ssh txjp "curl -s -o /dev/null -w '%{http_code}' -k 'https://monitor.tlyq.ai/api/health' 2>/dev/null" 2>/dev/null || echo "???")
if [[ "$STATUS" == "200" ]]; then
log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 https://monitor.tlyq.ai"
exit 0
else
warn "返回状态码: $STATUS,请检查"
exit 1
fi