871 lines
25 KiB
Bash
Executable File
871 lines
25 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
set -e
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
cd "$SCRIPT_DIR"
|
||
|
||
RED='\033[38;5;203m'
|
||
GREEN='\033[38;5;114m'
|
||
YELLOW='\033[38;5;221m'
|
||
BLUE='\033[38;5;111m'
|
||
CYAN='\033[38;5;117m'
|
||
WHITE='\033[1;37m'
|
||
DIM='\033[2m'
|
||
NC='\033[0m'
|
||
WAIT_SPINNER_FRAMES=(
|
||
'⠉⠁'
|
||
'⠈⠉'
|
||
'⠀⠙'
|
||
'⠀⠸'
|
||
'⠀⢰'
|
||
'⠀⣠'
|
||
'⢀⣀'
|
||
'⣀⡀'
|
||
'⣄⠀'
|
||
'⡆⠀'
|
||
'⠇⠀'
|
||
'⠋⠀'
|
||
)
|
||
WAIT_SPINNER_STEP=0
|
||
WAIT_SPINNER_TICKS_PER_SECOND=8
|
||
|
||
BACKEND_MAX_RETRIES="${BACKEND_MAX_RETRIES:-3}"
|
||
BACKEND_HEALTH_CHECK_ATTEMPTS="${BACKEND_HEALTH_CHECK_ATTEMPTS:-10}"
|
||
BACKEND_HEALTH_CHECK_INTERVAL="${BACKEND_HEALTH_CHECK_INTERVAL:-2}"
|
||
DEPENDENCY_INSTALL_MAX_RETRIES="${DEPENDENCY_INSTALL_MAX_RETRIES:-3}"
|
||
DEPENDENCY_INSTALL_RETRY_INTERVAL="${DEPENDENCY_INSTALL_RETRY_INTERVAL:-5}"
|
||
AI_PROVIDER_HEALTH_CHECK_ATTEMPTS="${AI_PROVIDER_HEALTH_CHECK_ATTEMPTS:-10}"
|
||
AI_PROVIDER_HEALTH_CHECK_INTERVAL="${AI_PROVIDER_HEALTH_CHECK_INTERVAL:-2}"
|
||
AI_PROVIDER_START_MAX_RETRIES="${AI_PROVIDER_START_MAX_RETRIES:-3}"
|
||
AI_PROVIDER_RETRY_INTERVAL="${AI_PROVIDER_RETRY_INTERVAL:-5}"
|
||
DATABASE_START_MAX_RETRIES="${DATABASE_START_MAX_RETRIES:-3}"
|
||
DATABASE_RETRY_INTERVAL="${DATABASE_RETRY_INTERVAL:-5}"
|
||
FRONTEND_MAX_RETRIES="${FRONTEND_MAX_RETRIES:-3}"
|
||
FRONTEND_HEALTH_CHECK_ATTEMPTS="${FRONTEND_HEALTH_CHECK_ATTEMPTS:-10}"
|
||
FRONTEND_HEALTH_CHECK_INTERVAL="${FRONTEND_HEALTH_CHECK_INTERVAL:-2}"
|
||
DEFAULT_BACKEND_PORT="${DEFAULT_BACKEND_PORT:-8000}"
|
||
DEFAULT_FRONTEND_PORT="${DEFAULT_FRONTEND_PORT:-3000}"
|
||
DEFAULT_AI_PROVIDER_PORT="${DEFAULT_AI_PROVIDER_PORT:-8010}"
|
||
|
||
compose_up() {
|
||
if docker compose version >/dev/null 2>&1; then
|
||
docker compose "$@"
|
||
else
|
||
docker-compose "$@"
|
||
fi
|
||
}
|
||
|
||
render_wait_spinner() {
|
||
local message="$1"
|
||
local step="${2:-1}"
|
||
local frame_index=$(( (step - 1) % ${#WAIT_SPINNER_FRAMES[@]} ))
|
||
local frame="${WAIT_SPINNER_FRAMES[$frame_index]}"
|
||
printf "\r${DIM}·${NC} ${CYAN}%s${NC} ${WHITE}%s${NC}\033[K" "$frame" "$message"
|
||
}
|
||
|
||
animate_wait_spinner() {
|
||
local message="$1"
|
||
local duration="${2:-1}"
|
||
local ticks=$(( duration * WAIT_SPINNER_TICKS_PER_SECOND ))
|
||
local tick=0
|
||
|
||
if [ "$ticks" -lt 1 ]; then
|
||
ticks=1
|
||
fi
|
||
|
||
while [ "$tick" -lt "$ticks" ]; do
|
||
WAIT_SPINNER_STEP=$((WAIT_SPINNER_STEP + 1))
|
||
render_wait_spinner "$message" "$WAIT_SPINNER_STEP"
|
||
sleep 0.125
|
||
tick=$((tick + 1))
|
||
done
|
||
}
|
||
|
||
clear_wait_spinner() {
|
||
printf "\r\033[K"
|
||
}
|
||
|
||
finish_wait_spinner() {
|
||
log_line "done" "$GREEN" "$1"
|
||
}
|
||
|
||
log_line() {
|
||
local label="$1"
|
||
local color="$2"
|
||
local message="$3"
|
||
clear_wait_spinner
|
||
printf "${DIM}·${NC} ${color}%-4s${NC} ${WHITE}%s${NC}\n" "$label" "$message"
|
||
}
|
||
|
||
log_step() {
|
||
log_line "run" "$BLUE" "$1"
|
||
}
|
||
|
||
log_warn() {
|
||
log_line "warn" "$YELLOW" "$1"
|
||
}
|
||
|
||
log_error() {
|
||
log_line "fail" "$RED" "$1"
|
||
}
|
||
|
||
log_note() {
|
||
printf "${DIM} %s${NC}\n" "$1"
|
||
}
|
||
|
||
log_success() {
|
||
log_line "done" "$GREEN" "$1"
|
||
}
|
||
|
||
print_splash() {
|
||
clear_wait_spinner
|
||
printf "%b" "$CYAN"
|
||
cat <<'EOF'
|
||
___ _ _ _ _ _
|
||
|_ _|_ __ | |_ ___| | (_) __ _ ___ _ __ | |_
|
||
| || '_ \| __/ _ \ | | |/ _` |/ _ \ '_ \| __|
|
||
| || | | | || __/ | | | (_| | __/ | | | |_
|
||
|___|_| |_|\__\___|_|_|_|\__, |\___|_| |_|\__|
|
||
|___/
|
||
EOF
|
||
printf "%b\n" "$NC"
|
||
printf "%b" "$BLUE"
|
||
cat <<'EOF'
|
||
____ _ _
|
||
| _ \| | __ _ _ __ ___ | |_
|
||
| |_) | |/ _` | '_ \ / _ \| __|
|
||
| __/| | (_| | | | | __/| |_
|
||
|_| |_|\__,_|_| |_|\___| \__|
|
||
EOF
|
||
printf "%b\n" "$NC"
|
||
printf "%b" "$WHITE"
|
||
cat <<'EOF'
|
||
____ _
|
||
| _ \| | __ _ _ __
|
||
| |_) | |/ _` | '_ \
|
||
| __/| | (_| | | | |
|
||
|_| |_|\__,_|_| |_|
|
||
EOF
|
||
printf "%b\n\n" "$NC"
|
||
}
|
||
|
||
run_with_retry() {
|
||
local max_retries="$1"
|
||
local interval="$2"
|
||
local failure_message="$3"
|
||
local retry_label="$4"
|
||
shift 4
|
||
|
||
local attempt=1
|
||
|
||
while [ "$attempt" -le "$max_retries" ]; do
|
||
if "$@"; then
|
||
return 0
|
||
fi
|
||
|
||
if [ "$attempt" -eq "$max_retries" ]; then
|
||
clear_wait_spinner
|
||
log_error "${failure_message}"
|
||
return 1
|
||
fi
|
||
|
||
animate_wait_spinner "${retry_label} 第 ${attempt}/${max_retries} 次失败,${interval} 秒后重试" "$interval"
|
||
attempt=$((attempt + 1))
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
ensure_uv_backend_deps() {
|
||
log_step "检查后端 uv 环境"
|
||
|
||
if ! command -v uv >/dev/null 2>&1; then
|
||
log_error "未找到 uv,请先安装 uv 并加入 PATH"
|
||
exit 1
|
||
fi
|
||
|
||
cd "$SCRIPT_DIR"
|
||
|
||
if [ ! -x "$SCRIPT_DIR/.venv/bin/python" ]; then
|
||
log_warn "未检测到 .venv,正在执行 uv sync"
|
||
if ! run_with_retry \
|
||
"$DEPENDENCY_INSTALL_MAX_RETRIES" \
|
||
"$DEPENDENCY_INSTALL_RETRY_INTERVAL" \
|
||
"uv 环境初始化失败,已重试 ${DEPENDENCY_INSTALL_MAX_RETRIES} 次" \
|
||
"uv sync" \
|
||
uv sync --group dev; then
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
if [ ! -x "$SCRIPT_DIR/.venv/bin/python" ]; then
|
||
log_error "uv 环境初始化失败,未找到 .venv/bin/python"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
ensure_frontend_deps() {
|
||
log_step "检查前端依赖"
|
||
|
||
if ! command -v bun >/dev/null 2>&1; then
|
||
log_error "未找到 bun,请先安装或加载 bun 到 PATH"
|
||
exit 1
|
||
fi
|
||
|
||
cd "$SCRIPT_DIR/frontend"
|
||
|
||
if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then
|
||
log_warn "前端依赖缺失,正在执行 bun install"
|
||
if ! run_with_retry \
|
||
"$DEPENDENCY_INSTALL_MAX_RETRIES" \
|
||
"$DEPENDENCY_INSTALL_RETRY_INTERVAL" \
|
||
"前端依赖安装失败,已重试 ${DEPENDENCY_INSTALL_MAX_RETRIES} 次" \
|
||
"bun install" \
|
||
bun install; then
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then
|
||
log_error "前端依赖安装失败,未找到 vite"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
wait_for_http() {
|
||
local url="$1"
|
||
local attempts="$2"
|
||
local interval="$3"
|
||
local service_name="$4"
|
||
local attempt=1
|
||
|
||
while [ "$attempt" -le "$attempts" ]; do
|
||
if curl -s "$url" > /dev/null 2>&1; then
|
||
finish_wait_spinner "${service_name}已就绪"
|
||
return 0
|
||
fi
|
||
|
||
animate_wait_spinner "等待${service_name}就绪" "$interval"
|
||
attempt=$((attempt + 1))
|
||
done
|
||
|
||
clear_wait_spinner
|
||
return 1
|
||
}
|
||
|
||
wait_for_container_health() {
|
||
local container_name="$1"
|
||
local attempts="$2"
|
||
local interval="$3"
|
||
local service_name="$4"
|
||
local attempt=1
|
||
local status
|
||
|
||
while [ "$attempt" -le "$attempts" ]; do
|
||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || true)"
|
||
|
||
if [ "$status" = "healthy" ] || [ "$status" = "running" ]; then
|
||
finish_wait_spinner "${service_name}容器健康检查已通过"
|
||
return 0
|
||
fi
|
||
|
||
animate_wait_spinner "等待${service_name}容器健康检查通过" "$interval"
|
||
attempt=$((attempt + 1))
|
||
done
|
||
|
||
clear_wait_spinner
|
||
return 1
|
||
}
|
||
|
||
wait_for_database_health() {
|
||
wait_for_container_health "planet_postgres" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "PostgreSQL" &&
|
||
wait_for_container_health "planet_redis" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "Redis"
|
||
}
|
||
|
||
wait_for_postgres_health() {
|
||
wait_for_container_health "planet_postgres" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "PostgreSQL"
|
||
}
|
||
|
||
start_database_services() {
|
||
docker start planet_postgres planet_redis >/dev/null 2>&1 || compose_up up -d postgres redis >/dev/null 2>&1
|
||
}
|
||
|
||
restart_database_services() {
|
||
docker restart planet_postgres planet_redis >/dev/null 2>&1 || compose_up up -d postgres redis >/dev/null 2>&1
|
||
}
|
||
|
||
start_postgres_service() {
|
||
docker start planet_postgres >/dev/null 2>&1 || compose_up up -d postgres >/dev/null 2>&1
|
||
}
|
||
|
||
start_backend_with_retry() {
|
||
local backend_port="$1"
|
||
local retry=1
|
||
|
||
while [ "$retry" -le "$BACKEND_MAX_RETRIES" ]; do
|
||
pkill -f "uvicorn" 2>/dev/null || true
|
||
cd "$SCRIPT_DIR/backend"
|
||
PYTHONPATH="$SCRIPT_DIR/backend" nohup uv run --project "$SCRIPT_DIR" python -m uvicorn app.main:app --host 0.0.0.0 --port "$backend_port" --reload > /tmp/planet_backend.log 2>&1 &
|
||
BACKEND_PID=$!
|
||
|
||
if wait_for_http "http://localhost:${backend_port}/health" "$BACKEND_HEALTH_CHECK_ATTEMPTS" "$BACKEND_HEALTH_CHECK_INTERVAL" "后端"; then
|
||
return 0
|
||
fi
|
||
|
||
kill "$BACKEND_PID" 2>/dev/null || true
|
||
animate_wait_spinner "后端第 ${retry}/${BACKEND_MAX_RETRIES} 次启动未就绪,准备重试" "$BACKEND_HEALTH_CHECK_INTERVAL"
|
||
retry=$((retry + 1))
|
||
done
|
||
|
||
clear_wait_spinner
|
||
return 1
|
||
}
|
||
|
||
start_ai_provider_service() {
|
||
local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}"
|
||
local retry=1
|
||
|
||
log_step "启动 AI Provider"
|
||
|
||
while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do
|
||
if docker start planet_aiprovider >/dev/null 2>&1 || compose_up up -d aiprovider >/dev/null 2>&1; then
|
||
if wait_for_container_health "planet_aiprovider" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider" &&
|
||
wait_for_http "http://localhost:${ai_provider_port}/health" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider"; then
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
if [ "$retry" -eq "$AI_PROVIDER_START_MAX_RETRIES" ]; then
|
||
clear_wait_spinner
|
||
log_error "AI Provider 启动失败,已重试 ${AI_PROVIDER_START_MAX_RETRIES} 次"
|
||
docker logs --tail 20 planet_aiprovider 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
|
||
docker restart planet_aiprovider >/dev/null 2>&1 || true
|
||
animate_wait_spinner "AI Provider 第 ${retry}/${AI_PROVIDER_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$AI_PROVIDER_RETRY_INTERVAL"
|
||
retry=$((retry + 1))
|
||
done
|
||
|
||
clear_wait_spinner
|
||
log_error "AI Provider 启动失败"
|
||
docker logs --tail 20 planet_aiprovider 2>/dev/null || true
|
||
exit 1
|
||
}
|
||
|
||
start_frontend_with_retry() {
|
||
local frontend_port="$1"
|
||
local retry=1
|
||
|
||
while [ "$retry" -le "$FRONTEND_MAX_RETRIES" ]; do
|
||
pkill -f "vite" 2>/dev/null || true
|
||
pkill -f "bun run dev" 2>/dev/null || true
|
||
cd "$SCRIPT_DIR/frontend"
|
||
nohup bun run dev --port "$frontend_port" > /tmp/planet_frontend.log 2>&1 &
|
||
FRONTEND_PID=$!
|
||
|
||
if wait_for_http "http://localhost:${frontend_port}" "$FRONTEND_HEALTH_CHECK_ATTEMPTS" "$FRONTEND_HEALTH_CHECK_INTERVAL" "前端"; then
|
||
return 0
|
||
fi
|
||
|
||
kill "$FRONTEND_PID" 2>/dev/null || true
|
||
animate_wait_spinner "前端第 ${retry}/${FRONTEND_MAX_RETRIES} 次启动未就绪,准备重试" "$FRONTEND_HEALTH_CHECK_INTERVAL"
|
||
retry=$((retry + 1))
|
||
done
|
||
|
||
clear_wait_spinner
|
||
return 1
|
||
}
|
||
|
||
ensure_database_services_healthy() {
|
||
local retry=1
|
||
|
||
while [ "$retry" -le "$DATABASE_START_MAX_RETRIES" ]; do
|
||
if start_database_services && wait_for_database_health; then
|
||
return 0
|
||
fi
|
||
|
||
if [ "$retry" -eq "$DATABASE_START_MAX_RETRIES" ]; then
|
||
clear_wait_spinner
|
||
log_error "数据库启动失败,已重试 ${DATABASE_START_MAX_RETRIES} 次"
|
||
docker logs --tail 20 planet_postgres 2>/dev/null || true
|
||
docker logs --tail 20 planet_redis 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
|
||
docker restart planet_postgres planet_redis >/dev/null 2>&1 || true
|
||
animate_wait_spinner "数据库第 ${retry}/${DATABASE_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$DATABASE_RETRY_INTERVAL"
|
||
retry=$((retry + 1))
|
||
done
|
||
}
|
||
|
||
ensure_postgres_service_healthy() {
|
||
local retry=1
|
||
|
||
while [ "$retry" -le "$DATABASE_START_MAX_RETRIES" ]; do
|
||
if start_postgres_service && wait_for_postgres_health; then
|
||
return 0
|
||
fi
|
||
|
||
if [ "$retry" -eq "$DATABASE_START_MAX_RETRIES" ]; then
|
||
clear_wait_spinner
|
||
log_error "PostgreSQL 启动失败,已重试 ${DATABASE_START_MAX_RETRIES} 次"
|
||
docker logs --tail 20 planet_postgres 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
|
||
docker restart planet_postgres >/dev/null 2>&1 || true
|
||
animate_wait_spinner "PostgreSQL 第 ${retry}/${DATABASE_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$DATABASE_RETRY_INTERVAL"
|
||
retry=$((retry + 1))
|
||
done
|
||
}
|
||
|
||
restart_database_service() {
|
||
local retry=1
|
||
|
||
log_step "重启数据库"
|
||
|
||
while [ "$retry" -le "$DATABASE_START_MAX_RETRIES" ]; do
|
||
if restart_database_services && wait_for_database_health; then
|
||
sleep 3
|
||
return 0
|
||
fi
|
||
|
||
if [ "$retry" -eq "$DATABASE_START_MAX_RETRIES" ]; then
|
||
clear_wait_spinner
|
||
log_error "数据库重启失败,已重试 ${DATABASE_START_MAX_RETRIES} 次"
|
||
docker logs --tail 20 planet_postgres 2>/dev/null || true
|
||
docker logs --tail 20 planet_redis 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
|
||
animate_wait_spinner "数据库第 ${retry}/${DATABASE_START_MAX_RETRIES} 次重启后仍未健康,继续重试" "$DATABASE_RETRY_INTERVAL"
|
||
retry=$((retry + 1))
|
||
done
|
||
}
|
||
|
||
start_backend_service() {
|
||
local backend_port="$1"
|
||
local backend_port_requested="$2"
|
||
local ai_provider_port="${3:-$DEFAULT_AI_PROVIDER_PORT}"
|
||
|
||
log_step "启动数据库"
|
||
ensure_database_services_healthy
|
||
sleep 3
|
||
|
||
start_ai_provider_service "$ai_provider_port"
|
||
|
||
if [ "$backend_port_requested" -eq 1 ]; then
|
||
kill_port_if_requested "$backend_port" "后端"
|
||
fi
|
||
|
||
log_step "启动后端"
|
||
ensure_uv_backend_deps
|
||
if ! start_backend_with_retry "$backend_port"; then
|
||
log_error "后端启动失败,已重试 ${BACKEND_MAX_RETRIES} 次"
|
||
tail -10 /tmp/planet_backend.log
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
start_frontend_service() {
|
||
local frontend_port="$1"
|
||
local frontend_port_requested="$2"
|
||
|
||
if [ "$frontend_port_requested" -eq 1 ]; then
|
||
kill_port_if_requested "$frontend_port" "前端"
|
||
fi
|
||
|
||
log_step "启动前端"
|
||
ensure_frontend_deps
|
||
if ! start_frontend_with_retry "$frontend_port"; then
|
||
log_error "前端启动失败,已重试 ${FRONTEND_MAX_RETRIES} 次"
|
||
tail -10 /tmp/planet_frontend.log
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
validate_port() {
|
||
local port="$1"
|
||
|
||
if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
|
||
log_error "非法端口: ${port}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
kill_port_if_requested() {
|
||
local port="$1"
|
||
local service_name="$2"
|
||
|
||
log_warn "检测 ${service_name} 端口 ${port} 占用"
|
||
|
||
if command -v fuser >/dev/null 2>&1 && fuser "${port}/tcp" >/dev/null 2>&1; then
|
||
log_step "发现端口 ${port} 占用,正在终止"
|
||
fuser -k "${port}/tcp" >/dev/null 2>&1 || true
|
||
sleep 1
|
||
return 0
|
||
fi
|
||
|
||
if command -v lsof >/dev/null 2>&1; then
|
||
local pids
|
||
pids="$(lsof -ti tcp:"${port}" 2>/dev/null || true)"
|
||
if [ -n "$pids" ]; then
|
||
log_step "发现端口 ${port} 占用,正在终止"
|
||
kill $pids 2>/dev/null || true
|
||
sleep 1
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
log_success "端口 ${port} 未被占用"
|
||
}
|
||
|
||
parse_service_args() {
|
||
BACKEND_PORT="$DEFAULT_BACKEND_PORT"
|
||
FRONTEND_PORT="$DEFAULT_FRONTEND_PORT"
|
||
AI_PROVIDER_PORT="$DEFAULT_AI_PROVIDER_PORT"
|
||
BACKEND_PORT_REQUESTED=0
|
||
FRONTEND_PORT_REQUESTED=0
|
||
AI_PROVIDER_REQUESTED=0
|
||
DATABASE_REQUESTED=0
|
||
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
-b|--backend-port)
|
||
BACKEND_PORT_REQUESTED=1
|
||
if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then
|
||
BACKEND_PORT="$2"
|
||
shift 2
|
||
else
|
||
shift 1
|
||
fi
|
||
;;
|
||
-f|--frontend-port)
|
||
FRONTEND_PORT_REQUESTED=1
|
||
if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then
|
||
FRONTEND_PORT="$2"
|
||
shift 2
|
||
else
|
||
shift 1
|
||
fi
|
||
;;
|
||
-a|--ai-provider-port)
|
||
AI_PROVIDER_REQUESTED=1
|
||
if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then
|
||
AI_PROVIDER_PORT="$2"
|
||
shift 2
|
||
else
|
||
shift 1
|
||
fi
|
||
;;
|
||
-d|--database)
|
||
DATABASE_REQUESTED=1
|
||
shift 1
|
||
;;
|
||
*)
|
||
log_error "未知参数: $1"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
validate_port "$BACKEND_PORT"
|
||
validate_port "$FRONTEND_PORT"
|
||
validate_port "$AI_PROVIDER_PORT"
|
||
}
|
||
|
||
cleanup_exit_containers() {
|
||
local exit_containers
|
||
exit_containers="$(docker ps -a --filter status=exited -q 2>/dev/null || true)"
|
||
if [ -n "$exit_containers" ]; then
|
||
log_step "清理残留 Exit 容器"
|
||
echo "$exit_containers" | xargs -r docker rm -f >/dev/null 2>&1 || true
|
||
log_success "残留容器已清理"
|
||
fi
|
||
}
|
||
|
||
stop_backend_service() {
|
||
pkill -f "uvicorn" 2>/dev/null || true
|
||
}
|
||
|
||
stop_ai_provider_service() {
|
||
docker stop planet_aiprovider 2>/dev/null || true
|
||
}
|
||
|
||
stop_frontend_service() {
|
||
pkill -f "vite" 2>/dev/null || true
|
||
pkill -f "bun run dev" 2>/dev/null || true
|
||
}
|
||
|
||
create_user() {
|
||
local username
|
||
local password
|
||
local password_confirm
|
||
local email
|
||
local generated_email
|
||
local role="viewer"
|
||
local is_admin
|
||
|
||
ensure_uv_backend_deps
|
||
|
||
log_step "启动数据库"
|
||
ensure_postgres_service_healthy
|
||
sleep 2
|
||
|
||
log_step "创建用户"
|
||
read -r -p "用户名: " username
|
||
|
||
if [ -z "$username" ]; then
|
||
log_error "用户名不能为空"
|
||
exit 1
|
||
fi
|
||
|
||
generated_email="${username}@planet.local"
|
||
read -r -p "邮箱(留空自动使用 ${generated_email}): " email
|
||
if [ -z "$email" ]; then
|
||
email="$generated_email"
|
||
fi
|
||
|
||
read -r -s -p "密码: " password
|
||
echo ""
|
||
read -r -s -p "确认密码: " password_confirm
|
||
echo ""
|
||
|
||
if [ -z "$password" ]; then
|
||
log_error "密码不能为空"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "${#password}" -lt 8 ]; then
|
||
log_error "密码长度不能少于 8 位"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$password" != "$password_confirm" ]; then
|
||
log_error "两次输入的密码不一致"
|
||
exit 1
|
||
fi
|
||
|
||
read -r -p "是否为管理员? (Y/N): " is_admin
|
||
if [[ "$is_admin" =~ ^[Yy]$ ]]; then
|
||
role="super_admin"
|
||
fi
|
||
|
||
cd "$SCRIPT_DIR/backend"
|
||
PLANET_CREATEUSER_USERNAME="$username" \
|
||
PLANET_CREATEUSER_PASSWORD="$password" \
|
||
PLANET_CREATEUSER_EMAIL="$email" \
|
||
PLANET_CREATEUSER_ROLE="$role" \
|
||
PYTHONPATH="$SCRIPT_DIR/backend" \
|
||
"$SCRIPT_DIR/.venv/bin/python" - <<'PY'
|
||
import asyncio
|
||
import os
|
||
|
||
from sqlalchemy import text
|
||
|
||
from app.core.security import get_password_hash
|
||
from app.db.session import async_session_factory
|
||
from app.models.user import User
|
||
|
||
|
||
async def main():
|
||
username = os.environ["PLANET_CREATEUSER_USERNAME"].strip()
|
||
password = os.environ["PLANET_CREATEUSER_PASSWORD"]
|
||
email = os.environ["PLANET_CREATEUSER_EMAIL"].strip()
|
||
role = os.environ["PLANET_CREATEUSER_ROLE"].strip()
|
||
|
||
async with async_session_factory() as session:
|
||
result = await session.execute(
|
||
text("SELECT id FROM users WHERE username = :username OR email = :email"),
|
||
{"username": username, "email": email},
|
||
)
|
||
if result.fetchone():
|
||
raise SystemExit("用户名已存在")
|
||
|
||
user = User(
|
||
username=username,
|
||
email=email,
|
||
password_hash=get_password_hash(password),
|
||
role=role,
|
||
is_active=True,
|
||
)
|
||
session.add(user)
|
||
await session.commit()
|
||
|
||
|
||
asyncio.run(main())
|
||
PY
|
||
|
||
log_success "用户创建成功"
|
||
log_note "用户名: ${username}"
|
||
log_note "角色: ${role}"
|
||
log_note "邮箱: ${email}"
|
||
}
|
||
|
||
start() {
|
||
parse_service_args "$@"
|
||
cleanup_exit_containers
|
||
|
||
print_splash
|
||
log_step "启动智能星球计划"
|
||
|
||
start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" "$AI_PROVIDER_PORT"
|
||
start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED"
|
||
|
||
echo ""
|
||
log_success "启动完成"
|
||
log_note "前端: http://localhost:${FRONTEND_PORT}"
|
||
log_note "后端: http://localhost:${BACKEND_PORT}"
|
||
log_note "AI Provider: http://localhost:${AI_PROVIDER_PORT}"
|
||
}
|
||
|
||
stop() {
|
||
log_warn "停止服务"
|
||
stop_backend_service
|
||
stop_ai_provider_service
|
||
stop_frontend_service
|
||
docker stop planet_postgres planet_redis 2>/dev/null || true
|
||
log_success "已停止"
|
||
}
|
||
|
||
restart() {
|
||
parse_service_args "$@"
|
||
cleanup_exit_containers
|
||
|
||
if [ "$BACKEND_PORT_REQUESTED" -eq 0 ] && [ "$FRONTEND_PORT_REQUESTED" -eq 0 ] && [ "$AI_PROVIDER_REQUESTED" -eq 0 ] && [ "$DATABASE_REQUESTED" -eq 0 ]; then
|
||
stop
|
||
sleep 1
|
||
start
|
||
return 0
|
||
fi
|
||
|
||
log_warn "按需重启服务"
|
||
|
||
if [ "$DATABASE_REQUESTED" -eq 1 ]; then
|
||
restart_database_service
|
||
fi
|
||
|
||
if [ "$AI_PROVIDER_REQUESTED" -eq 1 ]; then
|
||
stop_ai_provider_service
|
||
sleep 1
|
||
start_ai_provider_service "$AI_PROVIDER_PORT"
|
||
fi
|
||
|
||
if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then
|
||
stop_backend_service
|
||
sleep 1
|
||
start_backend_service "$BACKEND_PORT" 1 "$AI_PROVIDER_PORT"
|
||
fi
|
||
|
||
if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then
|
||
stop_frontend_service
|
||
sleep 1
|
||
start_frontend_service "$FRONTEND_PORT" 1
|
||
fi
|
||
|
||
echo ""
|
||
log_success "重启完成"
|
||
if [ "$DATABASE_REQUESTED" -eq 1 ]; then
|
||
log_note "数据库: planet_postgres, planet_redis"
|
||
fi
|
||
if [ "$AI_PROVIDER_REQUESTED" -eq 1 ]; then
|
||
log_note "AI Provider: http://localhost:${AI_PROVIDER_PORT}"
|
||
fi
|
||
if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then
|
||
log_note "后端: http://localhost:${BACKEND_PORT}"
|
||
fi
|
||
if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then
|
||
log_note "前端: http://localhost:${FRONTEND_PORT}"
|
||
fi
|
||
}
|
||
|
||
health() {
|
||
echo -e "${DIM}·${NC} ${BLUE}view${NC} ${WHITE}容器状态${NC}"
|
||
docker ps --filter "name=planet_" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||
|
||
echo ""
|
||
echo -e "${DIM}·${NC} ${BLUE}view${NC} ${WHITE}服务状态${NC}"
|
||
if curl -s http://localhost:8000/health > /dev/null 2>&1; then
|
||
echo -e "${DIM} 后端:${NC} ${GREEN}online${NC}"
|
||
else
|
||
echo -e "${DIM} 后端:${NC} ${RED}offline${NC}"
|
||
fi
|
||
|
||
if curl -s "http://localhost:${DEFAULT_AI_PROVIDER_PORT}/health" > /dev/null 2>&1; then
|
||
echo -e "${DIM} AI Provider:${NC} ${GREEN}online${NC}"
|
||
else
|
||
echo -e "${DIM} AI Provider:${NC} ${RED}offline${NC}"
|
||
fi
|
||
|
||
if curl -s http://localhost:3000 > /dev/null 2>&1; then
|
||
echo -e "${DIM} 前端:${NC} ${GREEN}online${NC}"
|
||
else
|
||
echo -e "${DIM} 前端:${NC} ${RED}offline${NC}"
|
||
fi
|
||
}
|
||
|
||
log() {
|
||
case "$1" in
|
||
-f|--frontend)
|
||
echo "📝 前端日志 (Ctrl+C 退出):"
|
||
tail -f /tmp/planet_frontend.log
|
||
;;
|
||
-b|--backend)
|
||
echo "📝 后端日志 (Ctrl+C 退出):"
|
||
tail -f /tmp/planet_backend.log
|
||
;;
|
||
-a|--ai-provider)
|
||
echo "📝 AI Provider 日志 (Ctrl+C 退出):"
|
||
docker logs -f planet_aiprovider
|
||
;;
|
||
*)
|
||
echo "📝 最近日志:"
|
||
echo "--- 后端 ---"
|
||
tail -20 /tmp/planet_backend.log 2>/dev/null || echo "无日志"
|
||
echo "--- AI Provider ---"
|
||
docker logs --tail 20 planet_aiprovider 2>/dev/null || echo "无日志"
|
||
echo "--- 前端 ---"
|
||
tail -20 /tmp/planet_frontend.log 2>/dev/null || echo "无日志"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
case "$1" in
|
||
start)
|
||
shift
|
||
start "$@"
|
||
;;
|
||
stop)
|
||
stop
|
||
;;
|
||
restart)
|
||
shift
|
||
restart "$@"
|
||
;;
|
||
createuser)
|
||
create_user
|
||
;;
|
||
health)
|
||
health
|
||
;;
|
||
log)
|
||
log "$2"
|
||
;;
|
||
*)
|
||
echo "用法: ./planet.sh {start|stop|restart|createuser|health|log}"
|
||
echo ""
|
||
echo "命令:"
|
||
echo " start 启动服务,可选: -b <后端端口> -f <前端端口> -a <AI Provider 端口>"
|
||
echo " stop 停止服务"
|
||
echo " restart 重启服务,可选: -b [后端端口] -f [前端端口] -a [AI Provider 端口] -d"
|
||
echo " createuser 交互创建用户"
|
||
echo " health 检查健康状态"
|
||
echo " log 查看日志"
|
||
echo " log -f 查看前端日志"
|
||
echo " log -b 查看后端日志"
|
||
echo " log -a 查看 AI Provider 日志"
|
||
;;
|
||
esac
|