Files
AIRegulation-DocAnalysis/dev.sh

754 lines
19 KiB
Bash
Raw Normal View History

#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"
VENV_DIR=".venv"
VENV_PYTHON="$VENV_DIR/bin/python"
LOG_DIR="logs"
API_PID_FILE="$LOG_DIR/api.pid"
FRONTEND_PID_FILE="$LOG_DIR/frontend.pid"
API_LOG_FILE="$LOG_DIR/api.log"
FRONTEND_LOG_FILE="$LOG_DIR/frontend.log"
DISPLAY_HOST="localhost"
SERVICE_HOST="6.86.80.8"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
load_env() {
load_env_file ".env"
load_env_file ".env.development"
API_HOST="${API_HOST:-0.0.0.0}"
API_PORT="${API_PORT:-8000}"
FRONTEND_PORT="${FRONTEND_PORT:-5173}"
FRONTEND_MODE="${FRONTEND_MODE:-dev}"
}
load_env_file() {
local env_file="$1"
if [ -f "$env_file" ]; then
while IFS='=' read -r key value; do
key="${key%$'\r'}"
value="${value%$'\r'}"
case "$key" in
""|\#*)
continue
;;
API_HOST|API_PORT|FRONTEND_PORT|FRONTEND_MODE)
export "$key=$value"
;;
esac
done < "$env_file"
fi
}
ensure_log_dir() {
mkdir -p "$LOG_DIR"
}
check_tcp_connectivity() {
local host="$1"
local port="$2"
if command -v nc > /dev/null 2>&1; then
nc -z -w 3 "$host" "$port" > /dev/null 2>&1
return
fi
require_python_bootstrap
"$PYTHON_BOOTSTRAP" - <<PY > /dev/null 2>&1
import socket
import sys
try:
with socket.create_connection(("${host}", ${port}), timeout=3):
pass
except Exception:
sys.exit(1)
sys.exit(0)
PY
}
check_foundation_services() {
local name
local port
for service in \
"Milvus:19530" \
"MinIO API:9000" \
"MinIO Console:9001" \
"Redis:6379" \
"PostgreSQL:5432"
do
name="${service%%:*}"
port="${service##*:}"
if check_tcp_connectivity "$SERVICE_HOST" "$port"; then
success "${name}: ${SERVICE_HOST}:${port} 可连通"
else
warn "${name}: ${SERVICE_HOST}:${port} 不可连通"
fi
done
}
print_header() {
echo ""
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} $1${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
}
info() {
echo -e "${CYAN}$1${NC}"
}
success() {
echo -e "${GREEN}$1${NC}"
}
warn() {
echo -e "${YELLOW}$1${NC}"
}
error() {
echo -e "${RED}$1${NC}" >&2
}
die() {
error "$1"
exit 1
}
is_pid_running() {
local pid="$1"
[ -n "$pid" ] && ps -p "$pid" > /dev/null 2>&1
}
read_pid() {
local pid_file="$1"
if [ -f "$pid_file" ]; then
cat "$pid_file"
fi
}
cleanup_stale_pid() {
local pid_file="$1"
local pid
pid="$(read_pid "$pid_file")"
if [ -n "$pid" ] && ! is_pid_running "$pid"; then
rm -f "$pid_file"
fi
}
port_pid() {
local port="$1"
if command -v lsof > /dev/null 2>&1; then
lsof -ti tcp:"$port" 2>/dev/null | head -n 1 || true
return 0
fi
if command -v ss > /dev/null 2>&1; then
ss -lptn "sport = :$port" 2>/dev/null | awk -F 'pid=' 'NR>1 && NF>1 {split($2,a,/,/); print a[1]; exit}' || true
return 0
fi
if command -v netstat > /dev/null 2>&1; then
netstat -lntp 2>/dev/null | awk -v port=":$port" '$4 ~ port {split($7,a,"/"); if (a[1] != "-") {print a[1]; exit}}' || true
fi
return 0
}
require_python_bootstrap() {
if command -v python3 > /dev/null 2>&1; then
PYTHON_BOOTSTRAP="python3"
elif command -v python > /dev/null 2>&1; then
PYTHON_BOOTSTRAP="python"
else
die "未找到 Python请先安装 Python 3.10+。"
fi
}
require_venv() {
[ -x "$VENV_PYTHON" ] || die "虚拟环境不存在,请先运行 ./dev.sh setup"
}
ensure_frontend_deps() {
if [ ! -d "frontend" ]; then
die "前端目录不存在: frontend"
fi
if [ ! -d "frontend/node_modules" ] || [ ! -d "frontend/node_modules/vite" ]; then
warn "前端依赖不存在或不完整,正在执行 npm install..."
npm --prefix frontend install
fi
}
validate_frontend_mode() {
case "$1" in
dev|static)
;;
*)
die "前端模式仅支持 dev 或 static当前值: $1"
;;
esac
}
check_python_version() {
local python_cmd="$1"
"$python_cmd" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)'
}
run_setup() {
load_env
ensure_log_dir
print_header "AI+合规智能中枢 - 环境初始化"
require_python_bootstrap
info "[1/4] 检查 Python 版本"
check_python_version "$PYTHON_BOOTSTRAP" || die "需要 Python 3.10+"
success "Python 版本检查通过"
echo ""
info "[2/4] 准备虚拟环境"
if [ ! -d "$VENV_DIR" ]; then
"$PYTHON_BOOTSTRAP" -m venv "$VENV_DIR"
success "已创建虚拟环境: $VENV_DIR"
else
success "虚拟环境已存在: $VENV_DIR"
fi
"$VENV_PYTHON" -m pip install --upgrade pip
"$VENV_PYTHON" -m pip install -r backend/requirements.txt
success "后端依赖安装完成"
echo ""
info "[3/4] 准备前端依赖"
if ! command -v npm > /dev/null 2>&1; then
die "未找到 npm请先安装 Node.js 20+。"
fi
npm --prefix frontend install
success "前端依赖安装完成"
echo ""
info "[4/4] 检查 6.86.80.8 基础服务连通性"
check_foundation_services
echo ""
success "环境初始化完成"
echo "后续常用命令:"
echo " ./dev.sh start"
echo " ./dev.sh status"
echo " ./dev.sh logs api --follow"
}
api_health_ok() {
if command -v curl > /dev/null 2>&1; then
curl -fsS "http://${DISPLAY_HOST}:$API_PORT/health" > /dev/null 2>&1
return
fi
require_venv
"$VENV_PYTHON" - <<PY > /dev/null 2>&1
import sys
from urllib.request import urlopen
try:
with urlopen("http://${DISPLAY_HOST}:${API_PORT}/health", timeout=3) as response:
body = response.read().decode("utf-8", errors="ignore")
sys.exit(0 if "healthy" in body.lower() else 1)
except Exception:
sys.exit(1)
PY
}
start_api() {
local mode="${1:-background}"
load_env
ensure_log_dir
require_venv
cleanup_stale_pid "$API_PID_FILE"
local pid
pid="$(read_pid "$API_PID_FILE")"
if is_pid_running "$pid"; then
warn "API 已在运行 (PID: $pid)"
return
fi
export PYTHONPATH="backend${PYTHONPATH:+:$PYTHONPATH}"
if [ "$mode" = "foreground" ]; then
print_header "AI+合规智能中枢 - 启动 API"
echo "运行模式: 前台调试(带 --reload"
echo "服务地址: http://${DISPLAY_HOST}:$API_PORT"
echo "文档地址: http://${DISPLAY_HOST}:$API_PORT/docs"
echo "健康检查: http://${DISPLAY_HOST}:$API_PORT/health"
echo ""
exec "$VENV_PYTHON" -m uvicorn app.main:app --host "$API_HOST" --port "$API_PORT" --reload
fi
nohup "$VENV_PYTHON" -m uvicorn app.main:app --host "$API_HOST" --port "$API_PORT" > "$API_LOG_FILE" 2>&1 &
pid=$!
echo "$pid" > "$API_PID_FILE"
sleep 3
if is_pid_running "$pid"; then
success "API 启动成功 (PID: $pid)"
echo " 地址: http://${DISPLAY_HOST}:$API_PORT"
echo " 文档: http://${DISPLAY_HOST}:$API_PORT/docs"
echo " 日志: $API_LOG_FILE"
else
rm -f "$API_PID_FILE"
die "API 启动失败,请查看日志: $API_LOG_FILE"
fi
}
start_frontend() {
local mode="${1:-$FRONTEND_MODE}"
load_env
ensure_log_dir
cleanup_stale_pid "$FRONTEND_PID_FILE"
local pid
pid="$(read_pid "$FRONTEND_PID_FILE")"
if is_pid_running "$pid"; then
warn "前端已在运行 (PID: $pid)"
return
fi
if ! command -v npm > /dev/null 2>&1; then
die "未找到 npm请先安装 Node.js 20+。"
fi
ensure_frontend_deps
if [ "$mode" = "static" ]; then
require_python_bootstrap
npm --prefix frontend run build
nohup "$PYTHON_BOOTSTRAP" -m http.server "$FRONTEND_PORT" --bind 0.0.0.0 --directory frontend/dist > "$FRONTEND_LOG_FILE" 2>&1 &
else
nohup npm --prefix frontend run dev -- --host 0.0.0.0 --port "$FRONTEND_PORT" > "$FRONTEND_LOG_FILE" 2>&1 &
fi
pid=$!
echo "$pid" > "$FRONTEND_PID_FILE"
sleep 4
if is_pid_running "$pid"; then
success "前端启动成功 (PID: $pid)"
echo " 地址: http://${DISPLAY_HOST}:$FRONTEND_PORT"
echo " 模式: $mode"
echo " 日志: $FRONTEND_LOG_FILE"
else
rm -f "$FRONTEND_PID_FILE"
die "前端启动失败,请查看日志: $FRONTEND_LOG_FILE"
fi
}
kill_pid_file_process() {
local pid_file="$1"
local label="$2"
local pid
pid="$(read_pid "$pid_file")"
if ! is_pid_running "$pid"; then
rm -f "$pid_file"
warn "$label 未通过 PID 文件发现运行中的进程"
return 1
fi
kill "$pid" 2>/dev/null || true
sleep 2
if is_pid_running "$pid"; then
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$pid_file"
success "$label 已停止"
return 0
}
stop_api() {
load_env
ensure_log_dir
if kill_pid_file_process "$API_PID_FILE" "API"; then
return
fi
local port_listener
port_listener="$(port_pid "$API_PORT")"
if [ -n "$port_listener" ]; then
kill "$port_listener" 2>/dev/null || true
sleep 1
if is_pid_running "$port_listener"; then
kill -9 "$port_listener" 2>/dev/null || true
fi
success "已停止监听 API 端口 $API_PORT 的进程 (PID: $port_listener)"
else
warn "未发现运行中的 API 服务"
fi
rm -f "$API_PID_FILE"
}
stop_frontend() {
load_env
ensure_log_dir
if kill_pid_file_process "$FRONTEND_PID_FILE" "前端"; then
return
fi
local port_listener
port_listener="$(port_pid "$FRONTEND_PORT")"
if [ -n "$port_listener" ]; then
kill "$port_listener" 2>/dev/null || true
sleep 1
if is_pid_running "$port_listener"; then
kill -9 "$port_listener" 2>/dev/null || true
fi
success "已停止监听前端端口 $FRONTEND_PORT 的进程 (PID: $port_listener)"
else
warn "未发现运行中的前端服务"
fi
rm -f "$FRONTEND_PID_FILE"
}
run_status() {
load_env
ensure_log_dir
print_header "AI+合规智能中枢 - 服务状态"
cleanup_stale_pid "$API_PID_FILE"
cleanup_stale_pid "$FRONTEND_PID_FILE"
local api_running=false
local frontend_running=false
local pid
local port_listener
local service_name
local service_port
echo -e "${YELLOW}API 服务:${NC}"
pid="$(read_pid "$API_PID_FILE")"
if is_pid_running "$pid"; then
api_running=true
success " 状态: 运行中"
echo " PID: $pid"
else
port_listener="$(port_pid "$API_PORT")"
if [ -n "$port_listener" ]; then
api_running=true
success " 状态: 运行中 (无 PID 文件)"
echo " PID: $port_listener"
else
error " 状态: 已停止"
fi
fi
if [ "$api_running" = true ]; then
if api_health_ok; then
success " 健康检查: 正常"
else
warn " 健康检查: 未通过"
fi
fi
echo " 地址: http://${DISPLAY_HOST}:$API_PORT"
echo " 文档: http://${DISPLAY_HOST}:${API_PORT}/docs"
echo ""
echo -e "${YELLOW}前端服务:${NC}"
pid="$(read_pid "$FRONTEND_PID_FILE")"
if is_pid_running "$pid"; then
frontend_running=true
success " 状态: 运行中"
echo " PID: $pid"
else
port_listener="$(port_pid "$FRONTEND_PORT")"
if [ -n "$port_listener" ]; then
frontend_running=true
success " 状态: 运行中 (无 PID 文件)"
echo " PID: $port_listener"
else
error " 状态: 已停止"
fi
fi
echo " 模式: $FRONTEND_MODE"
echo " 地址: http://${DISPLAY_HOST}:$FRONTEND_PORT"
echo ""
echo -e "${YELLOW}基础服务连通性:${NC}"
for service in \
"Milvus:19530" \
"MinIO API:9000" \
"MinIO Console:9001" \
"Redis:6379" \
"PostgreSQL:5432"
do
service_name="${service%%:*}"
service_port="${service##*:}"
if check_tcp_connectivity "$SERVICE_HOST" "$service_port"; then
success " ${service_name}: ${SERVICE_HOST}:${service_port} 可连通"
else
warn " ${service_name}: ${SERVICE_HOST}:${service_port} 不可连通"
fi
done
echo ""
if [ "$api_running" = true ] && [ "$frontend_running" = true ]; then
success "所有核心服务均在运行"
else
warn "存在未运行的服务,可使用 ./dev.sh start 启动"
fi
}
run_logs() {
local target="${1:-}"
local follow="${2:-}"
local log_file
case "$target" in
api)
log_file="$API_LOG_FILE"
;;
frontend)
log_file="$FRONTEND_LOG_FILE"
;;
*)
die "请指定日志类型: api 或 frontend"
;;
esac
[ -f "$log_file" ] || die "日志文件不存在: $log_file"
if [ "$follow" = "--follow" ]; then
tail -f "$log_file"
else
tail -n 50 "$log_file"
fi
}
show_help() {
cat <<'EOF'
AI+合规智能中枢统一脚本
用法:
./dev.sh help
./dev.sh setup
./dev.sh start [all|api|frontend] [--foreground] [--mode dev|static]
./dev.sh stop [all|api|frontend]
./dev.sh restart [all|api|frontend] [--mode dev|static]
./dev.sh status
./dev.sh logs <api|frontend> [--follow]
命令说明:
help
输出完整帮助信息、默认端口、日志目录和常见示例。
setup
进行一次性的本地初始化。
包含 Python 版本检查、.venv 虚拟环境创建、后端依赖安装、前端 npm install、
以及 6.86.80.8 基础服务端口连通性检查。
start
启动服务。默认行为等同于 ./dev.sh start all。
可选目标:
all 同时启动 API 和前端。
api 只启动后端 API。
frontend 只启动前端。
可选参数:
--foreground 仅对 start api 生效,前台运行并开启 --reload便于调试。
--mode dev 前端使用 Vite 开发服务器,默认端口 5173。
--mode static 前端先执行 npm run build再以静态文件服务器方式启动。
stop
停止服务。默认行为等同于 ./dev.sh stop all。
会优先读取 logs/*.pidPID 文件失效时会回退到端口探测。
restart
先停止再启动,支持 all/api/frontend。
restart frontend --mode static 可直接切换前端启动模式。
status
查看 API、前端、6.86.80.8 基础服务的状态。
API 状态包含健康检查;前端状态包含当前模式和访问地址。
logs
查看日志。默认输出最后 50 行。
追加 --follow 后会持续跟踪日志输出。
默认约定:
API_HOST 默认 0.0.0.0
API_PORT 默认 8000
FRONTEND_PORT 默认 5173
FRONTEND_MODE 默认 dev
日志目录 logs/
PID 文件 logs/api.pid, logs/frontend.pid
常用示例:
./dev.sh setup
./dev.sh start
./dev.sh start api --foreground
./dev.sh start frontend --mode static
./dev.sh restart frontend --mode dev
./dev.sh status
./dev.sh logs api --follow
./dev.sh logs frontend
EOF
}
parse_target() {
local default_target="$1"
local candidate="${2:-}"
case "$candidate" in
all|api|frontend)
echo "$candidate"
;;
*)
echo "$default_target"
;;
esac
}
main() {
local command="${1:-help}"
local target="all"
local mode=""
local foreground=false
local log_target=""
shift || true
load_env
case "$command" in
help|-h|--help)
show_help
;;
setup)
run_setup
;;
start)
target="$(parse_target all "${1:-}")"
if [ "$target" != "all" ]; then
shift || true
fi
while [ $# -gt 0 ]; do
case "$1" in
--foreground)
foreground=true
;;
--mode)
shift || die "--mode 需要指定 dev 或 static"
mode="$1"
validate_frontend_mode "$mode"
;;
*)
die "未知参数: $1"
;;
esac
shift || true
done
case "$target" in
all)
[ "$foreground" = false ] || die "start all 不支持 --foreground请使用 start api --foreground"
print_header "AI+合规智能中枢 - 启动服务"
start_api background
start_frontend "${mode:-$FRONTEND_MODE}"
;;
api)
if [ "$foreground" = true ]; then
start_api foreground
else
print_header "AI+合规智能中枢 - 启动 API"
start_api background
fi
;;
frontend)
print_header "AI+合规智能中枢 - 启动前端"
start_frontend "${mode:-$FRONTEND_MODE}"
;;
esac
;;
stop)
target="$(parse_target all "${1:-}")"
print_header "AI+合规智能中枢 - 停止服务"
case "$target" in
all)
stop_frontend
stop_api
;;
api)
stop_api
;;
frontend)
stop_frontend
;;
esac
;;
restart)
target="$(parse_target all "${1:-}")"
if [ "$target" != "all" ]; then
shift || true
fi
while [ $# -gt 0 ]; do
case "$1" in
--mode)
shift || die "--mode 需要指定 dev 或 static"
mode="$1"
validate_frontend_mode "$mode"
;;
*)
die "未知参数: $1"
;;
esac
shift || true
done
print_header "AI+合规智能中枢 - 重启服务"
case "$target" in
all)
stop_frontend
stop_api
start_api background
start_frontend "${mode:-$FRONTEND_MODE}"
;;
api)
stop_api
start_api background
;;
frontend)
stop_frontend
start_frontend "${mode:-$FRONTEND_MODE}"
;;
esac
;;
status)
run_status
;;
logs)
log_target="${1:-}"
run_logs "$log_target" "${2:-}"
;;
*)
die "未知命令: $command。可使用 ./dev.sh help 查看帮助。"
;;
esac
}
main "$@"