338 lines
14 KiB
Bash
Executable File
338 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
||
#
|
||
# Qihu-AIFlow 智能自动化PaaS平台 - 一键启动脚本
|
||
# 支持 Docker Compose 环境和本地开发环境
|
||
#
|
||
set -e
|
||
|
||
# ============================================================
|
||
# 项目路径检测(兼容 Docker 和本地环境)
|
||
# ============================================================
|
||
if [ -d "/app" ]; then
|
||
PROJECT_DIR="/app"
|
||
else
|
||
PROJECT_DIR="/data/share/project"
|
||
fi
|
||
cd "$PROJECT_DIR"
|
||
|
||
# Python 后端目录(提前定义,供后续步骤使用)
|
||
PYTHON_DIR="$PROJECT_DIR/backend/python"
|
||
|
||
echo "========================================"
|
||
echo " Qihu-AIFlow 智能自动化PaaS平台"
|
||
echo " 版本: $(cat $PROJECT_DIR/VERSION 2>/dev/null || echo '1.0.0')"
|
||
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo "========================================"
|
||
|
||
# ============================================================
|
||
# Step 1: 加载配置
|
||
# ============================================================
|
||
echo "[1/5] 初始化配置..."
|
||
|
||
# Docker 环境:直接用 docker-compose.yml 传入的环境变量,不加载 .env
|
||
# 本地环境:加载 .env 兜底
|
||
if [ -f /.dockerenv ] || [ -f /run/.containerenv ]; then
|
||
echo " - Docker 环境,使用容器环境变量"
|
||
else
|
||
echo " - 本地环境"
|
||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||
set -a
|
||
source "$PROJECT_DIR/.env"
|
||
set +a
|
||
echo " - 已加载 .env 配置文件"
|
||
else
|
||
echo " - 未找到 .env 文件,使用默认值"
|
||
fi
|
||
fi
|
||
|
||
# 仅使用环境变量或默认值,不做.env与env的相互覆盖
|
||
MYSQL_HOST="${MYSQL_HOST:-localhost}"
|
||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||
MYSQL_USER="${MYSQL_USER:-root}"
|
||
MYSQL_PASSWORD="${MYSQL_PASSWORD:-pwd2026}"
|
||
MYSQL_DATABASE="${MYSQL_DATABASE:-aiflow}"
|
||
REDIS_HOST="${REDIS_HOST:-localhost}"
|
||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||
REDIS_PASSWORD="${REDIS_PASSWORD:-pwd2026}"
|
||
|
||
echo " MySQL : $MYSQL_HOST:$MYSQL_PORT/$MYSQL_DATABASE"
|
||
echo " Redis : $REDIS_HOST:$REDIS_PORT"
|
||
|
||
# ============================================================
|
||
# Step 1.5: Docker 环境额外等待(等待 MySQL 端口真正 ready)
|
||
# ============================================================
|
||
if [ -f /.dockerenv ] || [ -f /run/.containerenv ]; then
|
||
echo " - Docker 环境:等待 network/port 完全就绪 (10s)..."
|
||
sleep 10
|
||
echo " - 等待完成"
|
||
fi
|
||
|
||
# 自动检测 MySQL 客户端 SSL 参数兼容性
|
||
# Docker环境(含MariaDB)使用 --ssl=0, 本地MySQL 8.x使用 --ssl-mode=DISABLED
|
||
if [ -f /.dockerenv ] || [ -f /run/.containerenv ]; then
|
||
MYSQL_SSL_FLAG="--ssl=0"
|
||
else
|
||
if mysql --ssl=0 -e "SELECT 1" &>/dev/null 2>&1; then
|
||
MYSQL_SSL_FLAG="--ssl=0"
|
||
else
|
||
MYSQL_SSL_FLAG="--ssl-mode=DISABLED"
|
||
fi
|
||
fi
|
||
echo " MySQL SSL 参数: $MYSQL_SSL_FLAG"
|
||
|
||
# ============================================================
|
||
# Step 2: 等待 MySQL 就绪
|
||
# ============================================================
|
||
echo "[2/5] 等待 MySQL 就绪 (${MYSQL_HOST}:${MYSQL_PORT})..."
|
||
echo -n " "
|
||
MAX_RETRIES=60
|
||
RETRY_COUNT=0
|
||
while ! mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG -e "SELECT 1" &>/dev/null; do
|
||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
|
||
echo ""
|
||
echo " ================================================"
|
||
echo " [错误] MySQL 连接超时"
|
||
echo " ================================================"
|
||
echo " 目标: ${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}"
|
||
echo " 用户: ${MYSQL_USER}"
|
||
echo " 已尝试: ${RETRY_COUNT} 次 (共等待约 $((RETRY_COUNT * 2)) 秒)"
|
||
echo ""
|
||
echo " --- 网络诊断 ---"
|
||
echo ""
|
||
# Step A1: DNS 解析测试
|
||
echo " [1] DNS 解析测试:"
|
||
if command -v getent &>/dev/null; then
|
||
getent hosts "$MYSQL_HOST" 2>&1 && echo " DNS 解析成功" || echo " DNS 解析失败: $MYSQL_HOST 无法解析"
|
||
elif command -v nslookup &>/dev/null; then
|
||
nslookup "$MYSQL_HOST" 2>&1 || echo " DNS 解析失败"
|
||
else
|
||
echo " (无可用的DNS诊断工具)"
|
||
fi
|
||
echo ""
|
||
# Step A2: TCP 端口连通性测试 (nc)
|
||
echo " [2] TCP 端口连通性测试 (nc -zv ${MYSQL_HOST} ${MYSQL_PORT}):"
|
||
if command -v nc &>/dev/null; then
|
||
nc -zv "$MYSQL_HOST" "$MYSQL_PORT" 2>&1 || echo " TCP 端口不可达"
|
||
else
|
||
echo " (nc 命令不可用,跳过)"
|
||
fi
|
||
echo ""
|
||
# Step A3: TCP 端口连通性测试 (bash /dev/tcp)
|
||
echo " [3] TCP 端口连通性测试 (bash /dev/tcp):"
|
||
if timeout 3 bash -c "echo >/dev/tcp/${MYSQL_HOST}/${MYSQL_PORT}" 2>/dev/null; then
|
||
echo " TCP 端口 ${MYSQL_HOST}:${MYSQL_PORT} 可达"
|
||
else
|
||
echo " TCP 端口 ${MYSQL_HOST}:${MYSQL_PORT} 不可达"
|
||
fi
|
||
echo ""
|
||
# Step A4: MySQL 客户端连接测试(显示详细错误)
|
||
echo " [4] MySQL 客户端连接测试 (详细错误信息):"
|
||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG -e "SELECT 1" 2>&1 || true
|
||
echo ""
|
||
echo " --- 诊断结论 ---"
|
||
echo " 可能原因:"
|
||
echo " 1. MySQL 容器健康检查通过但 3306 端口尚未完全 ready"
|
||
echo " 2. Docker 容器间网络不通(检查是否在同一 Docker 网络)"
|
||
echo " 3. MySQL 用户名密码错误"
|
||
echo " 4. MySQL 服务虽然运行但未监听 3306 端口"
|
||
echo ""
|
||
echo " 排查命令 (在宿主机执行):"
|
||
echo " docker compose ps"
|
||
echo " docker compose logs mysql --tail 50"
|
||
echo " docker exec qihu-aiflow-mysql mysqladmin ping -h localhost -u root -ppwd2026"
|
||
echo " docker inspect qihu-aiflow -f '{{.HostConfig.NetworkMode}}'"
|
||
echo " docker network ls"
|
||
echo " ================================================"
|
||
exit 1
|
||
fi
|
||
# 每5次或首次输出进度
|
||
if [ $((RETRY_COUNT % 5)) -eq 0 ]; then
|
||
echo -n "."
|
||
fi
|
||
sleep 2
|
||
done
|
||
echo ""
|
||
echo " MySQL 已就绪 (尝试 $RETRY_COUNT 次)"
|
||
|
||
# ============================================================
|
||
# Step 3: 数据库初始化(幂等执行)
|
||
# ============================================================
|
||
echo "[3/5] 初始化数据库表结构..."
|
||
|
||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG \
|
||
-e "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null
|
||
|
||
echo " 使用数据库: $MYSQL_DATABASE"
|
||
|
||
MIGRATION_DIR="$PROJECT_DIR/migrations"
|
||
if [ -d "$MIGRATION_DIR" ]; then
|
||
# 幂等迁移:用 schema_migrations 记录已执行版本,避免每次启动重复执行(如重复插入演示数据)
|
||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -e "CREATE TABLE IF NOT EXISTS schema_migrations (version VARCHAR(255) NOT NULL PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;" 2>/dev/null
|
||
for sql_file in $(ls "$MIGRATION_DIR"/*.sql 2>/dev/null | sort); do
|
||
filename=$(basename "$sql_file")
|
||
version="${filename%.sql}"
|
||
already=$(mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -sN -e "SELECT 1 FROM schema_migrations WHERE version='$version' LIMIT 1;" 2>/dev/null)
|
||
if [ "$already" = "1" ]; then
|
||
echo " - 跳过(已执行): $filename"
|
||
else
|
||
echo " - 执行: $filename"
|
||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" < "$sql_file" 2>&1 | sed 's/^/ /' || echo " [警告] $filename 部分语句已跳过(可能已存在)"
|
||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -e "INSERT IGNORE INTO schema_migrations (version) VALUES ('$version');" 2>/dev/null
|
||
fi
|
||
done
|
||
echo " 数据库初始化完成"
|
||
|
||
# ============================================================
|
||
# Step 3.5: 智能修复 admin 密码哈希
|
||
# 仅在密码哈希不可用(NULL/空/无效格式)时才重置为默认密码
|
||
# 如果用户已经修改过密码(有效bcrypt哈希),则保持不变
|
||
# 注意: 此步骤为可选操作,失败不影响服务启动
|
||
# ============================================================
|
||
echo " - 检查 admin 密码哈希有效性..."
|
||
set +e # 临时允许错误
|
||
PYTHON_BIN=""
|
||
|
||
# 查找可用的 Python(优先虚拟环境,其次系统 Python)
|
||
if [ -f "$PROJECT_DIR/backend/python/.venv/bin/python3" ]; then
|
||
PYTHON_BIN="$PROJECT_DIR/backend/python/.venv/bin/python3"
|
||
elif command -v python3 &>/dev/null; then
|
||
PYTHON_BIN="python3"
|
||
fi
|
||
|
||
if [ -n "$PYTHON_BIN" ]; then
|
||
# 从数据库读取admin当前的密码哈希
|
||
CURRENT_HASH=$(mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG \
|
||
"$MYSQL_DATABASE" -sN -e "SELECT password_hash FROM users WHERE username = 'admin';" 2>/dev/null)
|
||
|
||
# 用Python验证哈希是否有效(bcrypt $2b$格式)
|
||
NEED_RESET=$($PYTHON_BIN -c "
|
||
import sys
|
||
hash_val = sys.argv[1] if len(sys.argv) > 1 else ''
|
||
if not hash_val:
|
||
print('YES')
|
||
elif not hash_val.startswith('\$2b\$') and not hash_val.startswith('\$2a\$'):
|
||
print('YES')
|
||
else:
|
||
# 哈希格式合法,不做强制覆盖(用户可能修改过密码)
|
||
print('NO')
|
||
" "$CURRENT_HASH" 2>/dev/null) || NEED_RESET="YES"
|
||
|
||
if [ "$NEED_RESET" = "YES" ]; then
|
||
ADMIN_HASH_FIX=$($PYTHON_BIN -c "
|
||
import bcrypt
|
||
valid_hash = bcrypt.hashpw(b'admin123', bcrypt.gensalt()).decode()
|
||
print(valid_hash)
|
||
" 2>/dev/null) || true
|
||
|
||
if [ -n "$ADMIN_HASH_FIX" ]; then
|
||
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG \
|
||
"$MYSQL_DATABASE" -e "UPDATE users SET password_hash = '$ADMIN_HASH_FIX' WHERE username = 'admin';" 2>/dev/null || true
|
||
echo " admin 密码哈希已重置: admin123 (bcrypt) [原因: 旧哈希无效或缺失]"
|
||
else
|
||
echo " [警告] 无法生成 bcrypt 哈希,跳过 admin 密码修复"
|
||
fi
|
||
else
|
||
echo " admin 密码哈希有效,保持用户当前密码不变"
|
||
fi
|
||
else
|
||
echo " [警告] Python 不可用,跳过 admin 密码检查(不影响正常启动)"
|
||
fi
|
||
set -e # 恢复错误退出
|
||
else
|
||
echo " [警告] 迁移脚本目录不存在: $MIGRATION_DIR"
|
||
fi
|
||
|
||
# ============================================================
|
||
# Step 4: 等待 Redis 就绪
|
||
# ============================================================
|
||
echo "[4/5] 等待 Redis 就绪 (${REDIS_HOST}:${REDIS_PORT})..."
|
||
echo -n " "
|
||
RETRY_COUNT=0
|
||
while ! redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_PASSWORD" --no-auth-warning ping &>/dev/null; do
|
||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||
if [ $RETRY_COUNT -ge 30 ]; then
|
||
echo ""
|
||
echo " [警告] Redis 未能在 30 次重试内就绪,将无缓存继续启动"
|
||
break
|
||
fi
|
||
if [ $((RETRY_COUNT % 5)) -eq 0 ]; then
|
||
echo -n "."
|
||
fi
|
||
sleep 1
|
||
done
|
||
if [ $RETRY_COUNT -lt 30 ]; then
|
||
echo ""
|
||
echo " Redis 已就绪"
|
||
fi
|
||
|
||
# ============================================================
|
||
# Step 5: Python 环境与启动
|
||
# ============================================================
|
||
echo "[5/5] 配置 Python 环境..."
|
||
|
||
if [ -d "$PYTHON_DIR/.venv" ]; then
|
||
echo " - 使用已存在的虚拟环境"
|
||
export PATH="$PYTHON_DIR/.venv/bin:$PATH"
|
||
elif [ -d "$PYTHON_DIR" ]; then
|
||
echo " - 创建虚拟环境..."
|
||
python3 -m venv "$PYTHON_DIR/.venv"
|
||
export PATH="$PYTHON_DIR/.venv/bin:$PATH"
|
||
echo " - 安装 Python 依赖..."
|
||
pip install -r "$PYTHON_DIR/requirements.txt" -i https://pypi.tuna.tsinghua.edu.cn/simple -q 2>&1 | sed 's/^/ /'
|
||
echo " - 依赖安装完成"
|
||
else
|
||
echo " [错误] Python 后端目录不存在: $PYTHON_DIR"
|
||
exit 1
|
||
fi
|
||
|
||
# 前端资源检查
|
||
if [ -d "$PROJECT_DIR/frontend/dist" ]; then
|
||
echo " - 前端资源已就绪"
|
||
else
|
||
echo " [警告] 前端资源目录不存在,可能无法访问页面"
|
||
fi
|
||
|
||
# ============================================================
|
||
# Nginx & 后端启动
|
||
# ============================================================
|
||
|
||
# 如果 nginx 可用,则启动(Docker 环境)
|
||
if command -v nginx &>/dev/null; then
|
||
# 自愈:确保媒体生成(视频/图像)等长请求不被 60s 代理超时中断
|
||
if [ -f /etc/nginx/conf.d/aiflow.conf ]; then
|
||
sed -i -E 's/proxy_read_timeout[[:space:]]+[0-9]+s;/proxy_read_timeout 600s;/; s/proxy_send_timeout[[:space:]]+[0-9]+s;/proxy_send_timeout 600s;/' /etc/nginx/conf.d/aiflow.conf 2>/dev/null || true
|
||
fi
|
||
|
||
echo ""
|
||
echo " Nginx 配置测试..."
|
||
if nginx -t 2>&1; then
|
||
echo " 启动 Nginx (端口 8080)..."
|
||
nginx
|
||
if [ $? -eq 0 ]; then
|
||
echo " Nginx 已启动"
|
||
else
|
||
echo " [警告] Nginx 启动失败,前端页面可能无法访问"
|
||
fi
|
||
else
|
||
echo " [警告] Nginx 配置有误,跳过启动"
|
||
fi
|
||
fi
|
||
|
||
# 创建知识库文件存储目录
|
||
mkdir -p "$PROJECT_DIR/data/knowledge"
|
||
echo " 知识库存储目录已就绪: $PROJECT_DIR/data/knowledge"
|
||
|
||
# 启动后端服务(前台运行 - 容器必须保持前台进程)
|
||
echo ""
|
||
echo "========================================"
|
||
echo " Qihu-AIFlow 启动完成"
|
||
echo " 前端地址: http://localhost:12888"
|
||
echo " 后端地址: http://localhost:18080"
|
||
echo " 默认账号: admin / admin123"
|
||
echo "========================================"
|
||
echo ""
|
||
|
||
cd "$PYTHON_DIR"
|
||
exec python -m uvicorn main:app --host 0.0.0.0 --port 18080 --log-level info
|