Files
AIRegulation-DocAnalysis/dev.sh

754 lines
19 KiB
Bash
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.

#!/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 "$@"