Files
YG_FT/scripts/start-dev.sh
2026-07-21 16:15:01 +08:00

278 lines
7.2 KiB
Bash
Executable File
Raw 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
set -Eeuo pipefail
set -m
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BACKEND_DIR="$ROOT_DIR/backend"
FRONTEND_DIR="$ROOT_DIR/frontend"
BACKEND_VENV="$BACKEND_DIR/.venv"
BACKEND_PYTHON="$BACKEND_VENV/bin/python"
DO_SETUP=false
CHECK_ONLY=false
BACKEND_PID=""
FRONTEND_PID=""
CONFIG_PATH=""
FRONTEND_PORT=""
BACKEND_PORT=""
usage() {
cat <<'EOF'
用法:
./scripts/start-dev.sh 启动前端和后端
./scripts/start-dev.sh --setup 同步本地依赖后启动
./scripts/start-dev.sh --check 只检查依赖、配置和数据库连接
环境变量:
PYTHON_BIN 创建虚拟环境时使用的 Python 3.12+ 可执行文件
BACKEND_CONFIG_FILE 指定其他后端 YAML 配置文件
FRONTEND_PORT 覆盖 YAML 中的前端端口
BACKEND_PORT 覆盖 YAML 中的后端端口
EOF
}
fail() {
echo "$*" >&2
exit 1
}
process_group_alive() {
local pid="$1"
[[ -n "$pid" ]] && kill -0 -- "-$pid" 2>/dev/null
}
stop_process_group() {
local pid="$1"
local attempt=0
[[ -n "$pid" ]] || return 0
if process_group_alive "$pid"; then
kill -TERM -- "-$pid" 2>/dev/null || true
while process_group_alive "$pid" && [[ $attempt -lt 50 ]]; do
sleep 0.1
attempt=$((attempt + 1))
done
if process_group_alive "$pid"; then
kill -KILL -- "-$pid" 2>/dev/null || true
fi
elif kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
fi
wait "$pid" 2>/dev/null || true
}
cleanup() {
trap - EXIT
stop_process_group "$FRONTEND_PID"
stop_process_group "$BACKEND_PID"
}
handle_signal() {
echo
echo "🛑 正在停止前端和后端……"
exit 130
}
port_in_use() {
local port="$1"
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
}
select_base_python() {
if [[ -n "${PYTHON_BIN:-}" ]]; then
printf '%s\n' "$PYTHON_BIN"
elif [[ -x /opt/miniconda3/bin/python3 ]]; then
printf '%s\n' /opt/miniconda3/bin/python3
else
command -v python3 || true
fi
}
setup_dependencies() {
local base_python
base_python="$(select_base_python)"
[[ -n "$base_python" ]] || fail "未找到 Python 3请通过 PYTHON_BIN 指定 Python 3.12+。"
"$base_python" -c \
'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)' \
|| fail "后端要求 Python 3.12+,当前为 $($base_python --version 2>&1)"
if [[ ! -x "$BACKEND_PYTHON" ]]; then
echo "📦 创建后端虚拟环境……"
"$base_python" -m venv "$BACKEND_VENV"
fi
echo "📦 同步后端依赖……"
"$BACKEND_PYTHON" -m pip install -r "$BACKEND_DIR/requirements.txt"
echo "📦 同步前端依赖……"
(cd "$FRONTEND_DIR" && npm ci)
if [[ -z "${BACKEND_CONFIG_FILE:-}" && ! -f "$BACKEND_DIR/config.yaml" ]]; then
cp "$BACKEND_DIR/config.example.yaml" "$BACKEND_DIR/config.yaml"
echo "📝 已根据 config.example.yaml 创建本地 config.yaml请填写数据库密码。"
fi
}
resolve_and_check_config() {
local port_values
CONFIG_PATH="$(
cd "$BACKEND_DIR"
"$BACKEND_PYTHON" -c \
'from app.core.config import _resolve_config_path; print(_resolve_config_path()[0])'
)" || fail "无法解析后端配置文件路径。"
[[ -f "$CONFIG_PATH" ]] || fail "配置文件不存在:$CONFIG_PATH"
port_values="$(
cd "$BACKEND_DIR"
"$BACKEND_PYTHON" -c \
'from app.core.config import get_settings; settings = get_settings(); print(settings.frontend_port, settings.backend_port)'
)" || fail "配置文件无法加载:$CONFIG_PATH"
read -r FRONTEND_PORT BACKEND_PORT <<< "$port_values"
[[ -n "$FRONTEND_PORT" && -n "$BACKEND_PORT" ]] \
|| fail "无法从配置文件读取前端和后端端口:$CONFIG_PATH"
}
check_dependencies() {
command -v node >/dev/null 2>&1 || fail "未找到 Node.js。"
command -v npm >/dev/null 2>&1 || fail "未找到 npm。"
command -v curl >/dev/null 2>&1 || fail "未找到 curl。"
command -v lsof >/dev/null 2>&1 || fail "未找到 lsof。"
[[ -x "$FRONTEND_DIR/node_modules/.bin/vite" ]] \
|| fail "前端依赖不完整,请先运行 ./scripts/start-dev.sh --setup。"
[[ -x "$BACKEND_PYTHON" ]] \
|| fail "后端虚拟环境不存在,请先运行 ./scripts/start-dev.sh --setup。"
"$BACKEND_PYTHON" -c 'import fastapi, psycopg, sqlalchemy, uvicorn, yaml' \
|| fail "后端依赖不完整,请先运行 ./scripts/start-dev.sh --setup。"
resolve_and_check_config
}
check_database() {
echo "🔍 检查 PostgreSQL 连接:$CONFIG_PATH"
(
cd "$BACKEND_DIR"
"$BACKEND_PYTHON" -c '
import psycopg
from app.core.config import get_settings
url = get_settings().database_url
prefix = "postgresql+psycopg://"
if url.startswith(prefix):
url = "postgresql://" + url[len(prefix):]
connection = psycopg.connect(url, connect_timeout=3)
connection.close()
'
) || fail "PostgreSQL 无法连接,请检查配置文件中的地址、用户名和密码:$CONFIG_PATH"
}
wait_for_url() {
local url="$1"
local service_name="$2"
local pid="$3"
local attempt=0
local max_attempts=30
while [[ $attempt -lt $max_attempts ]]; do
if curl -fsS --max-time 1 "$url" >/dev/null 2>&1; then
return 0
fi
if ! kill -0 "$pid" 2>/dev/null; then
echo "$service_name 进程已提前退出。" >&2
return 1
fi
sleep 1
attempt=$((attempt + 1))
done
echo "❌ 等待 $service_name 就绪超时:$url" >&2
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--setup)
DO_SETUP=true
;;
--check)
CHECK_ONLY=true
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
fail "未知参数:$1"
;;
esac
shift
done
if [[ "$DO_SETUP" == true ]]; then
setup_dependencies
fi
check_dependencies
check_database
if [[ "$CHECK_ONLY" == true ]]; then
echo "✅ 前端、后端依赖、配置和 PostgreSQL 连接均正常。"
exit 0
fi
port_in_use "$BACKEND_PORT" \
&& fail "后端端口 $BACKEND_PORT 已被占用,后端可能已经启动。"
port_in_use "$FRONTEND_PORT" \
&& fail "前端端口 $FRONTEND_PORT 已被占用,前端可能已经启动。"
trap cleanup EXIT
trap handle_signal INT TERM
echo "🚀 启动后端http://127.0.0.1:$BACKEND_PORT"
(
cd "$BACKEND_DIR"
exec "$BACKEND_PYTHON" -m uvicorn app.main:app \
--host 0.0.0.0 \
--port "$BACKEND_PORT" \
--reload
) &
BACKEND_PID=$!
wait_for_url "http://127.0.0.1:$BACKEND_PORT/modelTF/health" "后端" "$BACKEND_PID" \
|| fail "后端启动失败。"
echo "🚀 启动前端http://127.0.0.1:$FRONTEND_PORT"
(
cd "$FRONTEND_DIR"
export FRONTEND_PORT BACKEND_PORT
exec npm run dev -- --host 0.0.0.0 --port "$FRONTEND_PORT" --strictPort
) &
FRONTEND_PID=$!
wait_for_url "http://127.0.0.1:$FRONTEND_PORT" "前端" "$FRONTEND_PID" \
|| fail "前端启动失败。"
echo "✅ 前端和后端均已就绪,按 Ctrl+C 可同时停止。"
while kill -0 "$BACKEND_PID" 2>/dev/null && kill -0 "$FRONTEND_PID" 2>/dev/null; do
sleep 1
done
set +e
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
wait "$BACKEND_PID"
EXIT_CODE=$?
echo "❌ 后端已退出,正在停止前端。" >&2
else
wait "$FRONTEND_PID"
EXIT_CODE=$?
echo "❌ 前端已退出,正在停止后端。" >&2
fi
set -e
exit "$EXIT_CODE"